├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── build.rs ├── dockerbuild.sh ├── examples ├── echo.rs ├── hello.rs ├── invokee.rs ├── invoker.rs ├── longlived.rs └── shadow.rs ├── src ├── bindings.rs ├── error.rs ├── handler.rs ├── iotdata.rs ├── lambda.rs ├── lib.rs ├── log.rs ├── request.rs ├── runtime.rs ├── secret.rs └── shadow.rs ├── stubs ├── CMakeLists.txt ├── README.md ├── include │ └── shared │ │ └── greengrasssdk.h └── src │ └── greengrasssdk.c └── wrapper.h /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .dockercache -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | /stubs/build 5 | **/*.vscode 6 | .idea 7 | .dockercache 8 | .history -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | `aws-greengrass-core-sdk-rust` adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | #### 1.x Releases 7 | 8 | - `1.0.x` Releases - [1.0.0](#100) 9 | 10 | --- 11 | 12 | ## Unreleased 13 | 14 | #### Added 15 | 16 | #### Updated 17 | 18 | #### Deprecated 19 | 20 | #### Removed 21 | 22 | #### Fixed 23 | 24 | --- 25 | 26 | ## [1.0.0]() 27 | 28 | Released on 2020-03-24. 29 | 30 | #### Added 31 | 32 | - Initial release of aws-greengrass-core-sdk-rust. 33 | - Added by [Jack Wright](https://github.com/ayax79). 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | There are a few guidelines that we need contributors to follow so that we are able to process requests as efficiently as possible. 4 | If you have any questions or concerns please feel free to contact us at [opensource@nike.com](mailto:opensource@nike.com). 5 | 6 | ## Getting Started 7 | 8 | * Review our [Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md) 9 | * Make sure you have a [GitHub account](https://github.com/signup/free) 10 | * Submit a ticket for your issue, assuming one does not already exist. 11 | * Clearly describe the issue including steps to reproduce when it is a bug. 12 | * Make sure you fill in the earliest version that you know has the issue. 13 | * Fork the repository on GitHub 14 | 15 | ## Making Changes 16 | 17 | * Create a topic branch off of `master` before you start your work. 18 | * Please avoid working directly on the `master` branch. 19 | * Make commits of logical units. 20 | * Check for unnecessary whitespace with `git diff --check` before committing. 21 | * Write meaningful, descriptive commit messages. 22 | 23 | ## Submitting Changes 24 | 25 | * Push your changes to a topic branch in your fork of the repository. 26 | * Submit a pull request to the repository in the Nike-Inc organization. 27 | * After feedback has been given we expect responses within two weeks. 28 | After two weeks we may close the pull request if it isn't showing any activity. 29 | * Bug fixes or features that lack appropriate tests may not be considered for merge. 30 | * Changes that lower test coverage may not be considered for merge. 31 | 32 | # Additional Resources 33 | 34 | * [General GitHub documentation](https://help.github.com/) 35 | * [GitHub pull request documentation](https://help.github.com/send-pull-requests/) 36 | * [Nike's Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md) 37 | * [Nike's Individual Contributor License Agreement](https://www.clahub.com/agreements/Nike-Inc/nike-inc.github.io) 38 | * [Nike OSS](https://nike-inc.github.io/) 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aws_greengrass_core_rust" 3 | description = "Provides an idiomatic Rust wrapper around the AWS Greengrass Core C SDK to more easily enable Greengrass native lambda functions in Rust." 4 | version = "0.1.37" 5 | authors = ["Pete Matern ", "Jack Wright "] 6 | edition = "2018" 7 | license = "Apache-2.0" 8 | build = "build.rs" 9 | 10 | [features] 11 | default = [] 12 | mock = [] 13 | # Feature that must be turned on for coverage tools not to fail 14 | # For some reason they are having issues with the bindgen stuff, which isn't used for most tests anyways 15 | coverage = [ "uuid" ] 16 | 17 | [build-dependencies] 18 | bindgen = "0.52.0" 19 | cmake = "0.1" 20 | 21 | [dependencies] 22 | log = "^0.4" 23 | lazy_static = "^1.4" 24 | crossbeam-channel = "^0.4" 25 | serde = {version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | base64 = "0.12" 28 | uuid = {version = "0.8", features = ["v4"], optional = true } 29 | 30 | [dev-dependencies] 31 | uuid = {version = "0.8", features = ["v4"] } 32 | hyper = "0.13" 33 | tokio = { version = "0.2", features = ["full"] } 34 | futures = "0.3" 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.0.0-experimental 2 | FROM rust:1.42 as cargo-build 3 | 4 | ENV LLVM_CONFIG_PATH /usr/lib/llvm-7/bin/llvm-config 5 | 6 | RUN mkdir /build 7 | WORKDIR /build 8 | 9 | RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ 10 | unzip awscliv2.zip && \ 11 | ./aws/install && \ 12 | rm -rf aws awscliv2.zip 13 | 14 | RUN apt-get update && \ 15 | apt-get install -y \ 16 | build-essential \ 17 | clang \ 18 | cmake \ 19 | zip \ 20 | libuv1-dev \ 21 | binutils-dev \ 22 | libcurl4-openssl-dev \ 23 | libiberty-dev \ 24 | libelf-dev \ 25 | libdw-dev \ 26 | jq 27 | 28 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ 29 | | sh -s -- --no-modify-path --default-toolchain none -y && \ 30 | rustup component add clippy rustfmt 31 | 32 | RUN git clone https://github.com/aws/aws-greengrass-core-sdk-c && \ 33 | cd aws-greengrass-core-sdk-c && \ 34 | mkdir build && \ 35 | cd build && \ 36 | cmake .. && \ 37 | CC=arm-linux-gnueabihf-gcc make && \ 38 | CC=arm-linux-gnueabihf-gcc make install 39 | 40 | RUN which cargo-make || cargo install --debug cargo-make 41 | RUN which cargo-kcov || cargo install --debug cargo-kcov 42 | 43 | RUN cargo kcov --print-install-kcov-sh > ./kcov-install.sh && \ 44 | sh ./kcov-install.sh 45 | 46 | RUN useradd rust --user-group --create-home --shell /bin/bash --groups sudo 47 | 48 | WORKDIR /home/rust/src -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "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. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "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. 22 | 23 | "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). 24 | 25 | "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. 26 | 27 | "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." 28 | 29 | "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. 30 | 31 | 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. 32 | 33 | 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. 34 | 35 | 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: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | 39 | You must cause any modified files to carry prominent notices stating that You changed the files; and 40 | 41 | 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 42 | 43 | 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. 44 | 45 | 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. 46 | 47 | 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. 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | 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. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work 58 | 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. 59 | 60 | Copyright [yyyy] [name of copyright owner] 61 | 62 | Licensed under the Apache License, Version 2.0 (the "License"); 63 | you may not use this file except in compliance with the License. 64 | You may obtain a copy of the License at 65 | 66 | http://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, 70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 | See the License for the specific language governing permissions and 72 | limitations under the License. 73 | 74 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Rust Greengrass SDK 2 | Copyright 2020-present, Nike, Inc. (http://nike.com) 3 | All rights reserved. 4 | The Nike Rust Greengrass SDK software is licensed under the Apache-2.0 license found in the LICENSE.txt file included in the root directory of the project source tree. 5 | 6 | ** Free and Open-Source Software Notices: 7 | The Nike Rust Greengrass SDK software incorporates, depends upon, interacts with, or was developed using the free and open-source software components listed below. Please see the linked component sites for additional licensing, dependency, and use information and component source code: 8 | 9 | * AWS Greengrass Core C SDK 10 | project homepage/download site: https://aws.github.io/aws-greengrass-core-sdk-c/ 11 | https://github.com/aws/aws-greengrass-core-sdk-c 12 | project licensing notices: 13 | /LICENSE: 14 | Apache License 15 | Version 2.0, January 2004 16 | http://www.apache.org/licenses/ 17 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 18 | 1. Definitions. 19 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 20 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 21 | "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. 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 23 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 24 | "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. 25 | "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). 26 | "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. 27 | "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." 28 | "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. 29 | 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. 30 | 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. 31 | 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: 32 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 33 | You must cause any modified files to carry prominent notices stating that You changed the files; and 34 | 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 35 | 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. 36 | 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. 37 | 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. 38 | 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. 39 | 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. 40 | 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. 41 | 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. 42 | END OF TERMS AND CONDITIONS 43 | 44 | APPENDIX: How to apply the Apache License to your work. 45 | 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. 46 | 47 | Copyright [yyyy] [name of copyright owner] 48 | 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 49 | 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. 50 | 51 | /NOTICE.txt: 52 | AWS Greengrass Core C SDK 53 | Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 54 | 55 | This product includes software developed at 56 | Amazon Web Services, Inc. (http://aws.amazon.com/). 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Greengrass Core Rust SDK 2 | Provides an idiomatic Rust wrapper around the [AWS Greengrass Core C SDK](https://github.com/aws/aws-greengrass-core-sdk-c) to more easily enable Greengrass native lambda functions in Rust. 3 | 4 | ## Features 5 | * Publishing to MQTT topics 6 | * Registering handlers and receiving messages from MQTT topics 7 | * Logging to the Greengrass logging backend via the log crate 8 | * Acquiring Secrets 9 | 10 | ## Examples 11 | * [hello.rs](./examples/hello.rs) - Simple example for initializing the greengrass runtime and sending a message on a topic 12 | * [echo.rs](./examples/echo.rs) - Example that shows how to register a Handler with the greengrass runtime and listen for message. 13 | * [shadow.rs](./examples/shadow.rs) - Example showing how to acquire and manipulate shadow documents. 14 | * [longlived.rs](./examples/longlived.rs) - Example showing how to create a longlived greengrass lambda that exposes a http endpoint. 15 | * [invoker.rs](./examples/invoker.rs) - An example of invoking one lambda for another lambda. Should be used with [invokee.rs](./examples/invokee.rs) 16 | 17 | ### Building examples 18 | Examples can be built following the directions in Quick start. Use ```cargo build --example ``` to build. 19 | 20 | ## Quickstart 21 | 22 | ### Prerequisites and Requirements 23 | 24 | * Install the [Greengrass C SDK](https://github.com/aws/aws-greengrass-core-sdk-c) (fails on Mac OS X, see note below) 25 | * Install [Rust](https://www.rust-lang.org/) 26 | * Install the [AWS CLI](https://aws.amazon.com/cli/) 27 | * A Device running green grass version v1.6 or newer 28 | * Create and configure a Greengrass group as described in the [Getting started with Amazon Greengrass](https://docs.aws.amazon.com/greengrass/latest/developerguide/gg-gs.html) 29 | 30 | #### Note for Building on mac 31 | The C Greengrass SDK fails to build on Mac OS X. The stubs directory contains a simple stubbed version of the SDK that 32 | can be used for compiling against Mac OS X. 33 | 34 | To Install: 35 | 1. ```cd stubs``` 36 | 2. ```mkdir build && cd build``` 37 | 3. ```cmake ..``` 38 | 4. ```make``` 39 | 5. ```make install``` 40 | 41 | ### Create new cargo project 42 | 43 | ```cargo new --bin my_gg_lambda``` 44 | 45 | ### Add the library to the Cargo.toml 46 | [//]: <> ("Update from git url to version once published to crates.io") 47 | Additionally, defined the logging crate 48 | ```toml 49 | aws_greengrass_core_rust = "0.1.36" 50 | log = "^0.4" 51 | ``` 52 | 53 | ### Edit main.rs 54 | 1. Initialize logging, greengrass runtime, and register a Handler 55 | 56 | [//]: <> ("Update from git url to version once published to crates.io") 57 | 58 | ```rust 59 | use aws_greengrass_core_rust::Initializer; 60 | use aws_greengrass_core_rust::log as gglog; 61 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 62 | use log::{info, error, LevelFilter}; 63 | use aws_greengrass_core_rust::runtime::Runtime; 64 | 65 | struct HelloHandler; 66 | 67 | impl Handler for HelloHandler { 68 | fn handle(&self, ctx: LambdaContext) { 69 | info!("Received context: {:#?}", ctx); 70 | let msg = String::from_utf8(ctx.message).expect("Message was not a valid utf8 string"); 71 | info!("Received event: {}", msg); 72 | } 73 | } 74 | 75 | pub fn main() { 76 | gglog::init_log(LevelFilter::Info); 77 | let runtime = Runtime::default().with_handler(Some(Box::new(HelloHandler))); 78 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 79 | error!("Initialization failed: {}", e); 80 | std::process::exit(1); 81 | } 82 | } 83 | ``` 84 | 85 | ### Build and package your lambda function 86 | ```shell script 87 | cargo build --release 88 | zip zip -j my_gg_lambda.zip "./target/release/my_gg_lambda" 89 | ``` 90 | 91 | Note: The binaries must be built on the operating system and architecture you are deploying to. If you are not on linux (Mac OS/windows) you can use the docker build: 92 | ```./dockerbuild.sh cargo build``` 93 | 94 | This will only work for x86 builds. 95 | 96 | 97 | ### Deploy your lambda function 98 | Using the information you used when creating your Greengrass group: 99 | ```shell script 100 | aws lambda create-function \ 101 | --region aws-region \ 102 | --function-name my_gg_lambda_x86 \ 103 | --handler executable-name \ 104 | --role role-arn \ 105 | --zip-file fileb://file-name.zip \ 106 | --runtime arn:aws:greengrass:::runtime/function/executable 107 | 108 | aws lambda publish-version \ 109 | --function-name my_gg_lambda_x86 \ 110 | --region aws-region 111 | 112 | aws lambda create-alias \ 113 | --function-name my_gg_lambda_x86 \ 114 | --name alias-name \ 115 | --function-version version-number \ 116 | --region aws-region 117 | ``` 118 | Note: We recommend adding an architecture suffix like x86 or arm to the lambda name if you are planning on deploying to 119 | multiple architectures. 120 | 121 | ### Configure your lambda function in your greengrass group 122 | Follow the instructions found in [Configure the Lambda Function for AWS IoT Greengrass](https://docs.aws.amazon.com/greengrass/latest/developerguide/config-lambda.html) 123 | 124 | ### Further reading: 125 | * [Run Lambda Functions on the AWS IoT Greengrass Core](https://docs.aws.amazon.com/greengrass/latest/developerguide/lambda-functions.html) 126 | 127 | 128 | ### Testing in your project 129 | 130 | When the feature "mock" is turned during the test phase the various clients will: 131 | 132 | 1. Allow you outputs to be overridden 133 | 2. Save arguments that methods have been called with 134 | 135 | ### Example 136 | ```rust 137 | #[cfg(test)] 138 | mod test { 139 | use super::*; 140 | 141 | #[test] 142 | fn test_publish_str() { 143 | let topic = "foo"; 144 | let message = "this is my message"; 145 | 146 | let mocks = MockHolder::default().with_publish_raw_outputs(vec![Ok(())]); 147 | let client = IOTDataClient::default().with_mocks(mocks); 148 | let response = client.publish(topic, message).unwrap(); 149 | println!("response: {:?}", response); 150 | 151 | let PublishRawInput(raw_topic, raw_bytes, raw_read) = 152 | &client.mocks.publish_raw_inputs.borrow()[0]; 153 | assert_eq!(raw_topic, topic); 154 | } 155 | } 156 | ``` 157 | 158 | ## Building from source 159 | 160 | ## Building 161 | 162 | 1. ```cargo build``` 163 | 164 | ## Testing Mock feature 165 | The examples will not build appropriately when the mock feature is enabled. To run the tests you must skip the examples: 166 | ```cargo test --features mock --lib``` 167 | 168 | ## Testing with code coverage 169 | 170 | There are some issues with coverage tools running correctly with our bindgen configuration in build.rs. Most of the tests do not 171 | actually need this as bindings.rs contains a mock set of bindings. To get around the failure the feature "coverage" can be enabled. 172 | This will avoid the bindings being generate and disable the couple of spots where the real bindings are needed. 173 | 174 | ### Coverage with [grcov](https://github.com/mozilla/grcov) 175 | 1. Install [gperftools](https://github.com/gperftools/gperftools) 176 | 2. Install Rust nightly: ```rustup install nightly``` 177 | 3. Install grcov: ```cargo +nightly install grcov``` 178 | 4. Set the following environment variables: 179 | ```shell script 180 | export CARGO_INCREMENTAL=0 181 | export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads" 182 | ``` 183 | 5. Build with coverage information: 184 | ```shell script 185 | cargo clean 186 | cargo +nightly build --features coverage 187 | cargo +nightly test --features coverage 188 | ``` 189 | 6. Run grcov: 190 | ```shell script 191 | grcov ./target/debug/ -s . -t html --llvm --branch --ignore-not-existing -o ./target/debug/coverage/ 192 | ``` 193 | 194 | ### Cross compilation ### 195 | 196 | ```shell script 197 | AWS_GREENGRASS_STUBS=yes CMAKE_TOOLCHAIN_FILE=$(pwd)/linux-gnu-x86_64.cmake cargo build --target=x86_64-unknown-linux-gnu 198 | ``` 199 | 200 | ### Coverage with [Jetbrains CLion](https://www.jetbrains.com/clion/) 201 | 1. Create a run coverage named Test 202 | 2. Set the command to be: ```test --features coverage``` 203 | 3. Run with coverage 204 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | use std::env; 10 | use std::path::PathBuf; 11 | 12 | fn main() { 13 | if cfg!(feature = "coverage") { 14 | return (); 15 | }; 16 | 17 | let mut builder = bindgen::Builder::default().header("wrapper.h"); 18 | 19 | if env::var("AWS_GREENGRASS_STUBS").is_ok() { 20 | let dst = cmake::build("stubs"); 21 | println!("cargo:rustc-link-search=native={}/lib", dst.display()); 22 | builder = builder.clang_arg(format!("-I{}/include", dst.display())); 23 | } 24 | 25 | println!("cargo:rustc-link-lib=aws-greengrass-core-sdk-c"); 26 | println!("cargo:rerun-if-changed=wrapper.h"); 27 | 28 | let bindings = builder 29 | .parse_callbacks(Box::new(bindgen::CargoCallbacks)) 30 | .generate() 31 | .expect("Unable to generate c bindings"); 32 | 33 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 34 | bindings 35 | .write_to_file(out_path.join("bindings.rs")) 36 | .expect("Unable to write bindings"); 37 | } 38 | -------------------------------------------------------------------------------- /dockerbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build -t aws-greengrass-core-sdk-rust-builder . 3 | docker run --rm -it \ 4 | -v "$(pwd)":/home/rust/src \ 5 | -v "$(pwd)/.dockercache/cargo-git":/home/rust/.cargo/git \ 6 | -v "$(pwd)/.dockercache/cargo-registry":/home/rust/.cargo/registry \ 7 | -v "$(pwd)/target/docker":/home/rust/src/target \ 8 | aws-greengrass-core-sdk-rust-builder $@ -------------------------------------------------------------------------------- /examples/echo.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! A Simple On Demand Lambda that registers a handler that listens to one MQTT topic and responds to another 10 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 11 | use aws_greengrass_core_rust::iotdata::IOTDataClient; 12 | use aws_greengrass_core_rust::log as gglog; 13 | use aws_greengrass_core_rust::runtime::{Runtime, RuntimeOption}; 14 | use aws_greengrass_core_rust::Initializer; 15 | use log::{error, info, LevelFilter}; 16 | 17 | /// The topic this Lambda will publish too. 18 | /// A different topic should be wired for this lambda to listen to. 19 | const SEND_TOPIC: &str = "gg_echo_lambda/device-sent"; 20 | 21 | struct EchoHandler; 22 | 23 | impl Handler for EchoHandler { 24 | fn handle(&self, ctx: LambdaContext) { 25 | info!("Handler received: {:?}", ctx); 26 | let message = String::from_utf8_lossy(ctx.message.as_slice()); 27 | info!("Message: {}", message); 28 | if let Err(e) = IOTDataClient::default().publish(SEND_TOPIC, ctx.message.clone()) { 29 | error!("Error sending {} to topic {} -- {}", message, SEND_TOPIC, e); 30 | } 31 | } 32 | } 33 | 34 | fn main() { 35 | gglog::init_log(LevelFilter::Debug); 36 | info!("Starting gg_echo_handler"); 37 | 38 | let runtime = Runtime::default() 39 | // On-demand greengrass lambdas should use RuntimeOption::Sync 40 | .with_runtime_option(RuntimeOption::Sync) 41 | .with_handler(Some(Box::new(EchoHandler))); 42 | 43 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 44 | error!("green grass initialization error: {}", e) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! This is a simple example that will just send a message to an MQTT topic when it is run. 10 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 11 | use aws_greengrass_core_rust::log as gglog; 12 | use aws_greengrass_core_rust::runtime::Runtime; 13 | use aws_greengrass_core_rust::Initializer; 14 | use log::{error, info, LevelFilter}; 15 | 16 | struct HelloHandler; 17 | 18 | impl Handler for HelloHandler { 19 | fn handle(&self, ctx: LambdaContext) { 20 | info!("Received context: {:#?}", ctx); 21 | let msg = String::from_utf8(ctx.message).expect("Message was not a valid utf8 string"); 22 | info!("Received event: {}", msg); 23 | } 24 | } 25 | 26 | pub fn main() { 27 | gglog::init_log(LevelFilter::Info); 28 | let runtime = Runtime::default().with_handler(Some(Box::new(HelloHandler))); 29 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 30 | error!("Initialization failed: {}", e); 31 | std::process::exit(1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/invokee.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! This is a simple example that will just send a message to an MQTT topic when it is run. 10 | //! 11 | //! This should be deployed in conjunction with the invoker example lambda 12 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 13 | use aws_greengrass_core_rust::lambda::LambdaClient; 14 | use aws_greengrass_core_rust::log as gglog; 15 | use aws_greengrass_core_rust::runtime::Runtime; 16 | use aws_greengrass_core_rust::Initializer; 17 | use log::{error, info, LevelFilter}; 18 | 19 | struct InvokeeHandler; 20 | 21 | impl Handler for InvokeeHandler { 22 | fn handle(&self, ctx: LambdaContext) { 23 | info!("Received context: {:?}", ctx); 24 | match String::from_utf8(ctx.message) { 25 | Ok(msg) => { 26 | info!("Received event: {}", msg); 27 | let reply = format!("{{\"original_msg\": \"{}\" }}", msg); 28 | if let Err(e) = LambdaClient::default().send_response(Ok(reply.as_bytes())) { 29 | error!("Error sending response: {}", e); 30 | } 31 | } 32 | Err(e) => { 33 | let reply = format!("Could not parse message: {}", e); 34 | error!("{}", reply); 35 | if let Err(e) = LambdaClient::default().send_response(Err(&reply)) { 36 | error!("Error sending error response: {}", e); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | pub fn main() { 44 | gglog::init_log(LevelFilter::Info); 45 | let runtime = Runtime::default().with_handler(Some(Box::new(InvokeeHandler))); 46 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 47 | error!("Initialization failed: {}", e); 48 | std::process::exit(1); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/invoker.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! An example of a lambda that takes a response that it receives on a queue and invokes another lambda 10 | //! And returns the response back. 11 | //! 12 | //! It is recommended that the invokee lambda is deployed along with this lambda. 13 | //! 14 | //! This can tested in the AWS IOT test tool by setting up two MQTT subscriptions: 15 | //! * IOT Cloud -> this lambda (i.e. invoker/device-rcvd) 16 | //! * this lambda -> iot cloud (i.e. invoker/device-sent) 17 | //! 18 | //! A request can be sent on from the test tool to the lambda like this (change arn and versions): 19 | //! ```json 20 | //! { 21 | //! "function_arn": "arn:aws:lambda:us-west-2:701603852992:function:invokee_x86:2", 22 | //! "qualifier": "2", 23 | //! "payload": "{\"msg\": \"hello lambda\"}", 24 | //! "response_topic": "invoker/device-sent" 25 | //! } 26 | //!``` 27 | //! Note: Determining the ARN for the greengrass lambda isn't easy in the console. The following command will work 28 | //! ```shell script 29 | //! aws lambda list-versions-by-function --function-name --output yaml 30 | //! ``` 31 | use aws_greengrass_core_rust::error::GGError; 32 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 33 | use aws_greengrass_core_rust::iotdata::IOTDataClient; 34 | use aws_greengrass_core_rust::lambda::{InvokeOptions, LambdaClient}; 35 | use aws_greengrass_core_rust::log as gg_log; 36 | use aws_greengrass_core_rust::runtime::Runtime; 37 | use aws_greengrass_core_rust::{GGResult, Initializer}; 38 | use log::{error, info, LevelFilter}; 39 | use serde::Deserialize; 40 | use serde_json::Value; 41 | 42 | pub fn main() { 43 | gg_log::init_log(LevelFilter::Debug); 44 | let runtime = Runtime::default().with_handler(Some(Box::new(InvokerHandler))); 45 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 46 | error!("Error initializing: {}", e); 47 | std::process::exit(1); 48 | } 49 | } 50 | 51 | struct InvokerHandler; 52 | 53 | impl Handler for InvokerHandler { 54 | fn handle(&self, ctx: LambdaContext) { 55 | info!("Received context: {:?}", ctx); 56 | if let Err(e) = invoke(&ctx.message) { 57 | error!("An error occurred handling event {}", e); 58 | } 59 | } 60 | } 61 | 62 | fn invoke(event: &[u8]) -> GGResult<()> { 63 | let req = InvokeRequest::from_slice(event)?; 64 | info!("Received event: {:?}", req); 65 | let options = build_invoke_options(&req)?; 66 | info!("Attempting to invoke {:?} with {:?}", options, req.payload); 67 | let resp = LambdaClient::default().invoke_sync(options, Some(req.payload))?; 68 | if let Some(resp) = resp { 69 | // convert the payload to a string for logging purposes 70 | let payload = String::from_utf8(resp).map_err(GGError::from)?; 71 | info!( 72 | "Responding to topic: {} with payload {}", 73 | req.response_topic, payload 74 | ); 75 | IOTDataClient::default().publish(&req.response_topic, payload)?; 76 | } 77 | Ok(()) 78 | } 79 | 80 | fn build_invoke_options(req: &InvokeRequest) -> GGResult> { 81 | // not really doing anything interesting with the context, so just put something that parses in it 82 | let context = serde_json::from_str(r#"{"foo": "bar"}"#).map_err(GGError::from)?; 83 | let opts = InvokeOptions::new(req.function_arn.clone(), context, req.qualifier.clone()); 84 | Ok(opts) 85 | } 86 | 87 | /// Represents the json request that this lambda will respond too 88 | #[derive(Deserialize, Debug)] 89 | struct InvokeRequest { 90 | function_arn: String, 91 | qualifier: String, 92 | payload: String, 93 | response_topic: String, 94 | } 95 | 96 | impl InvokeRequest { 97 | fn from_slice(slice: &[u8]) -> GGResult { 98 | serde_json::from_slice(slice).map_err(GGError::from) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/longlived.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! An example of a long-lived green grass lambda. 10 | //! This example creates an http endpoint on the greengrass device. It will then forward 11 | //! messages it receives to the MQTT topic longlived/device-sent. 12 | //! 13 | //! See the following guide for long lived functions: https://docs.aws.amazon.com/greengrass/latest/developerguide/long-lived.html 14 | //! 15 | //! ## Sample Request 16 | //! ```shell script 17 | //! curl -vvvv -H "Content-Type: application/json" -d '{"msg": "hello"}' http://127.0.0.1:5020/ 18 | //! ``` 19 | use aws_greengrass_core_rust::iotdata::IOTDataClient; 20 | use aws_greengrass_core_rust::log as gglog; 21 | use aws_greengrass_core_rust::runtime::{Runtime, RuntimeOption}; 22 | use aws_greengrass_core_rust::{GGResult, Initializer}; 23 | use hyper::service::{make_service_fn, service_fn}; 24 | use hyper::{Body, Method, Request, Response, Server, StatusCode}; 25 | use log::{error, info, LevelFilter}; 26 | 27 | const SEND_TOPIC: &str = "longlived/device-sent"; 28 | 29 | async fn serve(req: Request) -> Result, hyper::Error> { 30 | match (req.method(), req.uri().path()) { 31 | // Simply echo the body back to the client. 32 | (&Method::POST, "/") => { 33 | let body = hyper::body::to_bytes(req.into_body()).await?; 34 | match publish(&body).await { 35 | Ok(_) => { 36 | let mut accepted = Response::default(); 37 | *accepted.status_mut() = StatusCode::ACCEPTED; 38 | Ok(accepted) 39 | } 40 | Err(e) => { 41 | error!("greengrass error occurred: {}", e); 42 | let mut internal_error = Response::new(Body::from(format!("{}", e))); 43 | *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; 44 | Ok(internal_error) 45 | } 46 | } 47 | } 48 | 49 | // Return the 404 Not Found for other routes. 50 | _ => { 51 | let mut not_found = Response::default(); 52 | *not_found.status_mut() = StatusCode::NOT_FOUND; 53 | Ok(not_found) 54 | } 55 | } 56 | } 57 | 58 | async fn publish(bytes: &[u8]) -> GGResult<()> { 59 | // convert to a string for logging purposes 60 | info!("publishing message of {}", String::from_utf8_lossy(bytes)); 61 | IOTDataClient::default().publish(SEND_TOPIC, bytes) 62 | } 63 | 64 | #[tokio::main] 65 | async fn main() -> Result<(), Box> { 66 | // Initialize logging 67 | gglog::init_log(LevelFilter::Debug); 68 | 69 | // Initialize Greengrass, long lived functions need to be configured with RuntimeOption::Async 70 | let runtime = Runtime::default().with_runtime_option(RuntimeOption::Async); 71 | Initializer::default().with_runtime(runtime).init()?; 72 | 73 | // Initialize hyper 74 | let addr = ([0, 0, 0, 0], 5020).into(); 75 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(serve)) }); 76 | let server = Server::bind(&addr).serve(service); 77 | info!("Listening on http://{}", addr); 78 | server.await?; 79 | info!("longlived lambda exiting"); 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /examples/shadow.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! An on-demand lambda function that will manipulate a specified thing's shadow document. 10 | //! This lambda will listen on an MQTT topic for a json command body. The following actions can be performed: 11 | //! 12 | //! # Get Shadow Document 13 | //! This will acquire a shadow document for a specified device and send it back to a specified MQTT Topic 14 | //! 15 | //! ## Example Payload: 16 | //! ```json 17 | //! { 18 | //! "command": "GET", 19 | //! "thing_name": "myThingName" 20 | //! }``` 21 | //! 22 | //! # Delete Shadow Document 23 | //! Delete the shadow document for a specified device 24 | //! 25 | //! ## Example Payload 26 | //! ```json 27 | //! { 28 | //! "command": "DELETE", 29 | //! "thing_name": "myThingName" 30 | //! }``` 31 | //! 32 | //! # Update Shadow Document 33 | //! Update the shadow document for a specified device 34 | //! ```json 35 | //! { 36 | //! "command": "UPDATE". 37 | //! "thing_name": "myThingName", 38 | //! "document": { 39 | //! "state" : { 40 | //! "desired" : { 41 | //! "color" : "RED", 42 | //! "sequence" : [ "RED", "GREEN", "BLUE" ] 43 | //! } 44 | //! } 45 | //! } 46 | //! }``` 47 | use aws_greengrass_core_rust::error::GGError; 48 | use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 49 | use aws_greengrass_core_rust::iotdata::IOTDataClient; 50 | use aws_greengrass_core_rust::log as gglog; 51 | use aws_greengrass_core_rust::runtime::Runtime; 52 | use aws_greengrass_core_rust::shadow::ShadowClient; 53 | use aws_greengrass_core_rust::{GGResult, Initializer}; 54 | use serde::{Deserialize, Serialize}; 55 | use serde_json::Value; 56 | use std::default::Default; 57 | 58 | use log::{error, info, LevelFilter}; 59 | 60 | const DEFAULT_SEND_TOPIC: &str = "/shadow-example/device-sent"; 61 | 62 | struct ShadowHandler { 63 | iot_data_client: IOTDataClient, 64 | shadow_client: ShadowClient, 65 | send_topic: String, 66 | } 67 | 68 | impl ShadowHandler { 69 | pub fn new() -> Self { 70 | let send_topic = std::env::var("SEND_TOPIC").unwrap_or(DEFAULT_SEND_TOPIC.to_owned()); 71 | 72 | ShadowHandler { 73 | iot_data_client: IOTDataClient::default(), 74 | shadow_client: ShadowClient::default(), 75 | send_topic, 76 | } 77 | } 78 | 79 | fn do_stuff_with_thing(&self, body: &[u8]) -> GGResult<()> { 80 | match serde_json::from_slice::(body) { 81 | Ok(ref command) => self.handle_command(command), 82 | Err(_) => { 83 | let response: Response = Response::default() 84 | .with_code(400) 85 | .with_message(Some("Did not receive a valid command object".to_owned())); 86 | self.publish(&response) 87 | } 88 | } 89 | } 90 | 91 | fn handle_command(&self, c: &Command) -> GGResult<()> { 92 | match c.r#type { 93 | // Get will attempt to grab a document and publish it to an MQTT topic 94 | CommandType::Get => self.handle_get(c), 95 | // Attempt to update a shadow thing based on the document we received 96 | CommandType::Update => self.handle_update(c), 97 | // Delete the shadow document for the specified thing 98 | CommandType::Delete => self.handle_delete(c), 99 | } 100 | } 101 | 102 | fn handle_get(&self, command: &Command) -> GGResult<()> { 103 | match self 104 | .shadow_client 105 | .get_thing_shadow::(&command.thing_name) 106 | { 107 | // We grabbed the document, send it. 108 | Ok(Some(thing)) => { 109 | info!("Shadow Thing: {:#?}", thing); 110 | let response = Response::default() 111 | .with_code(200) 112 | .with_body(Some(Box::new(thing))); 113 | self.publish(&response) 114 | } 115 | // We got a 404 back, respond 116 | Ok(_) => { 117 | info!("No shadow doc found for thing: {:?}", &command.thing_name); 118 | let response: Response = Response::default() 119 | .with_code(404) 120 | .with_message(Some("No shadow document for thing".to_owned())); 121 | self.publish(&response) 122 | } 123 | Err(ref e) => self.handle_error(e), 124 | } 125 | } 126 | 127 | fn handle_update(&self, command: &Command) -> GGResult<()> { 128 | if let Some(ref document) = command.document { 129 | match self 130 | .shadow_client 131 | .update_thing_shadow(&command.thing_name, &document) 132 | { 133 | Ok(_) => { 134 | let response: Response = Response::default() 135 | .with_code(200) 136 | .with_message(Some(format!( 137 | "Updated shadow for thing {} successfully", 138 | command.thing_name 139 | ))); 140 | self.publish(&response) 141 | } 142 | Err(ref e) => self.handle_error(e), 143 | } 144 | } else { 145 | let response: Response = 146 | Response::default() 147 | .with_code(400) 148 | .with_message(Some(format!( 149 | "No document specified to update thing: {}", 150 | command.thing_name 151 | ))); 152 | self.publish(&response) 153 | } 154 | } 155 | 156 | fn handle_delete(&self, command: &Command) -> GGResult<()> { 157 | match self.shadow_client.delete_thing_shadow(&command.thing_name) { 158 | Ok(_) => { 159 | let response: Response = Response::default() 160 | .with_code(200) 161 | .with_message(Some(format!( 162 | "Shadow for thing {} successfully deleted.", 163 | command.thing_name 164 | ))); 165 | self.publish(&response) 166 | } 167 | Err(ref e) => self.handle_error(e), 168 | } 169 | } 170 | 171 | fn handle_error(&self, err: &GGError) -> GGResult<()> { 172 | match err { 173 | GGError::ErrorResponse(e) => { 174 | let code = e.error_response.as_ref().map(|er| er.code).unwrap_or(500); 175 | let response = Response::default() 176 | .with_code(code) 177 | .with_body(Some(Box::new(e.clone()))); 178 | self.publish(&response) 179 | } 180 | _ => { 181 | let response: Response = Response::default() 182 | .with_code(500) 183 | .with_message(Some(format!("Error occurred: {}", err))); 184 | self.publish(&response) 185 | } 186 | } 187 | } 188 | 189 | fn publish(&self, response: &Response) -> GGResult<()> { 190 | self.iot_data_client 191 | .publish_json(&self.send_topic, response) 192 | .map(|_| ()) 193 | } 194 | } 195 | 196 | impl Handler for ShadowHandler { 197 | fn handle(&self, ctx: LambdaContext) { 198 | if let Err(e) = self.do_stuff_with_thing(&ctx.message) { 199 | error!("Error calling shadows api: {}", e); 200 | } 201 | } 202 | } 203 | 204 | fn main() { 205 | gglog::init_log(LevelFilter::Debug); 206 | info!("Starting shadow gg lambda"); 207 | 208 | let runtime = Runtime::default().with_handler(Some(Box::new(ShadowHandler::new()))); 209 | 210 | if let Err(e) = Initializer::default().with_runtime(runtime).init() { 211 | error!("green grass initialization error: {}", e) 212 | } 213 | } 214 | 215 | #[derive(Deserialize)] 216 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 217 | enum CommandType { 218 | Update, 219 | Get, 220 | Delete, 221 | } 222 | 223 | #[derive(Deserialize)] 224 | struct Command { 225 | thing_name: String, 226 | r#type: CommandType, 227 | document: Option, 228 | } 229 | 230 | #[derive(Serialize, Default)] 231 | struct Response { 232 | code: u16, 233 | message: Option, 234 | body: Option>, 235 | } 236 | 237 | impl Response { 238 | fn with_code(self, code: u16) -> Self { 239 | Response { code, ..self } 240 | } 241 | 242 | fn with_message(self, message: Option) -> Self { 243 | Response { message, ..self } 244 | } 245 | 246 | fn with_body(self, body: Option>) -> Self { 247 | Response { body, ..self } 248 | } 249 | } 250 | 251 | /// Use to statisfy type constraints when there isn't a body 252 | #[derive(Serialize, Debug, Default)] 253 | struct EmptyBody; 254 | -------------------------------------------------------------------------------- /src/bindings.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | #![allow(dead_code, improper_ctypes, unused_variables, non_upper_case_globals, non_camel_case_types, 10 | non_snake_case, clippy::all)] 11 | //! This module encapsulates the bindings for the C library 12 | //! The bindings are regenerated on build on every build. 13 | //! For testing we do two things 14 | //! 15 | //! 1. Use a mocked version with test hooks for the rest of the project 16 | //! 2. Add another module so the tests against the generated bindings is still run 17 | //! 18 | //! improper c_types is ignored. This is do to the u128 issue described here: https://github.com/rust-lang/rust-bindgen/issues/1549 19 | //! dead_code is allowed, do to a number of things in the bindings not being used 20 | 21 | #[cfg(all(not(test), not(feature = "coverage")))] 22 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 23 | 24 | #[cfg(any(test, feature = "coverage"))] 25 | pub use self::test::*; 26 | 27 | /// Provides stubbed testing versions of methods, etc that match greengrasssdk.h 28 | /// Useful for internal testing. 29 | /// All test that utilize this package must have a #[cfg(not(feature = "mock"))] or the build will fail. 30 | #[cfg(any(test, feature = "coverage"))] 31 | pub mod test { 32 | use crate::handler::LambdaContext; 33 | use crate::lambda::InvokeType; 34 | use base64; 35 | use lazy_static::lazy_static; 36 | use std::cell::RefCell; 37 | use std::convert::TryFrom; 38 | use std::ffi::{CStr, CString}; 39 | use std::os::raw::c_void; 40 | use std::sync::Mutex; 41 | use std::thread_local; 42 | use uuid::Uuid; 43 | 44 | lazy_static! { 45 | // This could problems if more than than one test is accessing. Try to limit usage. 46 | pub(crate) static ref GG_HANDLER: Mutex = Mutex::new(None); 47 | } 48 | 49 | // Thread locals used for testing 50 | thread_local! { 51 | pub(crate) static GG_SHADOW_THING_ARG: RefCell = RefCell::new("".to_owned()); 52 | pub(crate) static GG_UPDATE_PAYLOAD: RefCell = RefCell::new("".to_owned()); 53 | pub(crate) static GG_REQUEST_READ_BUFFER: RefCell> = RefCell::new(vec![]); 54 | pub(crate) static GG_REQUEST: RefCell<_gg_request> = RefCell::new(_gg_request::default()); 55 | pub(crate) static GG_LAMBDA_HANDLER_READ_BUFFER: RefCell> = RefCell::new(vec![]); 56 | /// used to store the arguments passed to gg_publish 57 | pub(crate) static GG_PUBLISH_ARGS: RefCell = RefCell::new(GGPublishPayloadArgs::default()); 58 | pub(crate) static GG_PUBLISH_WITH_OPTIONS_ARGS: RefCell = RefCell::new(GGPublishPayloadArgs::default()); 59 | pub(crate) static GG_GET_SECRET_VALUE_ARGS: RefCell = RefCell::new(GGGetSecretValueArgs::default()); 60 | pub(crate) static GG_GET_SECRET_VALUE_RETURN: RefCell = RefCell::new(gg_error_GGE_SUCCESS); 61 | pub(crate) static GG_CLOSE_REQUEST_COUNT: RefCell = RefCell::new(0); 62 | pub(crate) static GG_PUBLISH_OPTION_INIT_COUNT: RefCell = RefCell::new(0); 63 | pub(crate) static GG_PUBLISH_OPTION_FREE_COUNT: RefCell = RefCell::new(0); 64 | pub(crate) static GG_INVOKE_ARGS: RefCell = RefCell::new(GGInvokeArgs::default()); 65 | pub(crate) static GG_PUBLISH_OPTIONS_SET_QUEUE_FULL_POLICY: RefCell = RefCell::new(1515); 66 | pub(crate) static GG_LOG_ARGS: RefCell> = RefCell::new(vec![]); 67 | pub(crate) static GG_LAMBDA_HANDLER_WRITE_RESPONSE: RefCell> = RefCell::new(vec![]); 68 | pub(crate) static GG_LAMBDA_HANDLER_WRITE_ERROR: RefCell = RefCell::new("".to_owned()); 69 | } 70 | 71 | pub fn reset_test_state() { 72 | GG_SHADOW_THING_ARG.with(|rc| rc.replace("".to_owned())); 73 | GG_UPDATE_PAYLOAD.with(|rc| rc.replace("".to_owned())); 74 | GG_REQUEST_READ_BUFFER.with(|rc| rc.replace(vec![])); 75 | GG_REQUEST.with(|rc| rc.replace(_gg_request::default())); 76 | GG_LAMBDA_HANDLER_READ_BUFFER.with(|rc| rc.replace(vec![])); 77 | GG_PUBLISH_ARGS.with(|rc| rc.replace(GGPublishPayloadArgs::default())); 78 | GG_PUBLISH_WITH_OPTIONS_ARGS.with(|rc| rc.replace(GGPublishPayloadArgs::default())); 79 | GG_GET_SECRET_VALUE_ARGS.with(|rc| rc.replace(GGGetSecretValueArgs::default())); 80 | GG_CLOSE_REQUEST_COUNT.with(|rc| rc.replace(0)); 81 | GG_PUBLISH_OPTION_INIT_COUNT.with(|rc| rc.replace(0)); 82 | GG_PUBLISH_OPTION_FREE_COUNT.with(|rc| rc.replace(0)); 83 | GG_GET_SECRET_VALUE_RETURN.with(|rc| rc.replace(gg_error_GGE_SUCCESS)); 84 | GG_PUBLISH_OPTIONS_SET_QUEUE_FULL_POLICY.with(|rc| rc.replace(1515)); 85 | GG_LOG_ARGS.with(|rc| rc.replace(vec![])); 86 | let mut handler = GG_HANDLER.lock().unwrap(); 87 | *handler = None; 88 | } 89 | 90 | #[derive(Debug, Copy, Clone, Default)] 91 | pub struct _gg_request { 92 | id: Option, 93 | } 94 | 95 | impl _gg_request { 96 | pub fn is_default(&self) -> bool { 97 | self.id.is_none() 98 | } 99 | } 100 | 101 | pub type gg_request = *mut _gg_request; 102 | pub const gg_request_status_GG_REQUEST_SUCCESS: gg_request_status = 0; 103 | pub const gg_request_status_GG_REQUEST_HANDLED: gg_request_status = 1; 104 | pub const gg_request_status_GG_REQUEST_UNHANDLED: gg_request_status = 2; 105 | pub const gg_request_status_GG_REQUEST_UNKNOWN: gg_request_status = 3; 106 | pub const gg_request_status_GG_REQUEST_AGAIN: gg_request_status = 4; 107 | pub const gg_request_status_GG_REQUEST_RESERVED_MAX: gg_request_status = 5; 108 | pub const gg_request_status_GG_REQUEST_RESERVED_PAD: gg_request_status = 2147483647; 109 | pub type gg_request_status = u32; 110 | #[repr(C)] 111 | #[derive(Debug, Copy, Clone)] 112 | pub struct gg_request_result { 113 | pub request_status: gg_request_status, 114 | } 115 | 116 | pub const gg_error_GGE_SUCCESS: gg_error = 0; 117 | pub const gg_error_GGE_OUT_OF_MEMORY: gg_error = 1; 118 | pub const gg_error_GGE_INVALID_PARAMETER: gg_error = 2; 119 | pub const gg_error_GGE_INVALID_STATE: gg_error = 3; 120 | pub const gg_error_GGE_INTERNAL_FAILURE: gg_error = 4; 121 | pub const gg_error_GGE_TERMINATE: gg_error = 5; 122 | pub const gg_error_GGE_RESERVED_MAX: gg_error = 6; 123 | pub const gg_error_GGE_RESERVED_PAD: gg_error = 2147483647; 124 | pub type gg_error = u32; 125 | 126 | pub const gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_BEST_EFFORT: 127 | gg_queue_full_policy_options = 0; 128 | pub const gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_ALL_OR_ERROR: 129 | gg_queue_full_policy_options = 1; 130 | pub const gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_RESERVED_MAX: 131 | gg_queue_full_policy_options = 2; 132 | pub const gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_RESERVED_PAD: 133 | gg_queue_full_policy_options = 2147483647; 134 | 135 | pub type gg_queue_full_policy_options = u32; 136 | 137 | #[derive(Debug, Copy, Clone)] 138 | pub struct _gg_publish_options { 139 | _unused: [u8; 0], 140 | } 141 | 142 | pub type gg_publish_options = *mut _gg_publish_options; 143 | 144 | pub const gg_log_level_GG_LOG_RESERVED_NOTSET: gg_log_level = 0; 145 | pub const gg_log_level_GG_LOG_DEBUG: gg_log_level = 1; 146 | pub const gg_log_level_GG_LOG_INFO: gg_log_level = 2; 147 | pub const gg_log_level_GG_LOG_WARN: gg_log_level = 3; 148 | pub const gg_log_level_GG_LOG_ERROR: gg_log_level = 4; 149 | pub const gg_log_level_GG_LOG_FATAL: gg_log_level = 5; 150 | pub const gg_log_level_GG_LOG_RESERVED_MAX: gg_log_level = 6; 151 | pub const gg_log_level_GG_LOG_RESERVED_PAD: gg_log_level = 2147483647; 152 | 153 | pub type gg_log_level = u32; 154 | 155 | pub extern "C" fn gg_global_init(opt: u32) -> gg_error { 156 | gg_error_GGE_SUCCESS 157 | } 158 | 159 | #[derive(PartialEq, Debug)] 160 | pub struct LogArgs { 161 | level: gg_log_level, 162 | format: String, 163 | } 164 | 165 | impl LogArgs { 166 | pub fn new(level: gg_log_level, format: &str) -> Self { 167 | LogArgs { 168 | level, 169 | format: format.to_owned(), 170 | } 171 | } 172 | } 173 | 174 | pub extern "C" fn gg_log( 175 | level: gg_log_level, 176 | format: *const ::std::os::raw::c_char, 177 | ) -> gg_error { 178 | unsafe { 179 | let format = CStr::from_ptr(format).to_owned().into_string().unwrap(); 180 | let args = LogArgs { level, format }; 181 | GG_LOG_ARGS.with(|rc| rc.borrow_mut().push(args)); 182 | } 183 | gg_error_GGE_SUCCESS 184 | } 185 | 186 | pub extern "C" fn gg_request_init(ggreq: *mut gg_request) -> gg_error { 187 | unsafe { 188 | let req = _gg_request { 189 | id: Some(Uuid::new_v4()), 190 | }; 191 | GG_REQUEST.with(|rc| { 192 | rc.replace(req); 193 | std::ptr::replace(ggreq, rc.as_ptr()) 194 | }); 195 | } 196 | gg_error_GGE_SUCCESS 197 | } 198 | 199 | pub extern "C" fn gg_request_close(ggreq: gg_request) -> gg_error { 200 | GG_CLOSE_REQUEST_COUNT.with(|rc| { 201 | let new_value = *rc.borrow() + 1; 202 | rc.replace(new_value); 203 | }); 204 | 205 | gg_error_GGE_SUCCESS 206 | } 207 | 208 | pub extern "C" fn gg_request_read( 209 | ggreq: gg_request, 210 | buffer: *mut ::std::os::raw::c_void, 211 | buffer_size: usize, 212 | amount_read: *mut usize, 213 | ) -> gg_error { 214 | unsafe { 215 | GG_REQUEST_READ_BUFFER.with(|b| { 216 | let mut borrowed = b.borrow().clone(); 217 | // If the vector is empty, don't do anything and notify that we read zero bytes 218 | if borrowed.is_empty() { 219 | amount_read.write(0); 220 | } else { 221 | // Find the index to split the array off at 222 | let index = if buffer_size > borrowed.len() { 223 | borrowed.len() 224 | } else { 225 | buffer_size 226 | }; 227 | // borrowed will now contain everything up to index 228 | let next = borrowed.split_off(index); 229 | println!("gg_request_read: writing buffer: {:?}", borrowed); 230 | buffer.copy_from_nonoverlapping( 231 | borrowed.as_ptr() as *const c_void, 232 | borrowed.len(), 233 | ); 234 | amount_read.write(borrowed.len()); 235 | // replace the refcell with the rest of the vec 236 | b.replace(next); 237 | } 238 | }); 239 | gg_error_GGE_SUCCESS 240 | } 241 | } 242 | 243 | #[derive(Debug, Copy, Clone)] 244 | pub struct gg_lambda_context { 245 | pub function_arn: *const ::std::os::raw::c_char, 246 | pub client_context: *const ::std::os::raw::c_char, 247 | } 248 | 249 | pub type gg_lambda_handler = 250 | ::std::option::Option; 251 | 252 | pub extern "C" fn gg_runtime_start(handler: gg_lambda_handler, opt: u32) -> gg_error { 253 | let mut current_handler = GG_HANDLER.lock().unwrap(); 254 | *current_handler = handler; 255 | gg_error_GGE_SUCCESS 256 | } 257 | 258 | /// Sets up the GG_LAMBDA_HANDLER_READ_BUFFER and calls the registered c handler 259 | pub(crate) fn send_to_handler(ctx: LambdaContext) { 260 | let message = ctx.message.clone(); 261 | GG_LAMBDA_HANDLER_READ_BUFFER.with(|rc| rc.replace(message)); 262 | let locked = GG_HANDLER.lock().unwrap(); 263 | if let Some(handler) = *locked { 264 | unsafe { 265 | let function_arn_c = CString::new(ctx.function_arn).unwrap().into_raw(); 266 | let client_ctx_c = CString::new(ctx.client_context.as_str()) 267 | .unwrap() 268 | .into_raw(); 269 | let ctx_c = Box::new(gg_lambda_context { 270 | function_arn: function_arn_c, 271 | client_context: client_ctx_c, 272 | }); 273 | let raw = Box::into_raw(ctx_c); 274 | handler(raw); 275 | // make sure things are cleaned up 276 | let _ = Box::from_raw(raw); 277 | let _ = CString::from_raw(function_arn_c); 278 | let _ = CString::from_raw(client_ctx_c); 279 | } 280 | } 281 | } 282 | 283 | pub extern "C" fn gg_lambda_handler_read( 284 | buffer: *mut ::std::os::raw::c_void, 285 | buffer_size: usize, 286 | amount_read: *mut usize, 287 | ) -> gg_error { 288 | unsafe { 289 | GG_LAMBDA_HANDLER_READ_BUFFER.with(|b| { 290 | let mut borrowed = b.borrow().clone(); 291 | // If the vector is empty, don't do anything and notify that we read zero bytes 292 | if borrowed.is_empty() { 293 | amount_read.write(0); 294 | } else { 295 | // Find the index to split the array off at 296 | let index = if buffer_size > borrowed.len() { 297 | borrowed.len() 298 | } else { 299 | buffer_size 300 | }; 301 | // borrowed will now contain everything up to index 302 | let next = borrowed.split_off(index); 303 | println!("gg_lambda_handler_read: writing buffer: {:?}", borrowed); 304 | buffer.copy_from_nonoverlapping( 305 | borrowed.as_ptr() as *const c_void, 306 | borrowed.len(), 307 | ); 308 | amount_read.write(borrowed.len()); 309 | // replace the refcell with the rest of the vec 310 | b.replace(next); 311 | } 312 | }); 313 | } 314 | gg_error_GGE_SUCCESS 315 | } 316 | 317 | pub extern "C" fn gg_lambda_handler_write_response( 318 | response: *const ::std::os::raw::c_void, 319 | response_size: usize, 320 | ) -> gg_error { 321 | GG_LAMBDA_HANDLER_WRITE_RESPONSE.with(|rc| unsafe { 322 | let mut dst: Vec = Vec::with_capacity(response_size); 323 | dst.set_len(response_size); 324 | std::ptr::copy(response as *const u8, dst.as_mut_ptr(), response_size); 325 | rc.replace(dst); 326 | }); 327 | gg_error_GGE_SUCCESS 328 | } 329 | 330 | pub extern "C" fn gg_lambda_handler_write_error( 331 | error_message: *const ::std::os::raw::c_char, 332 | ) -> gg_error { 333 | GG_LAMBDA_HANDLER_WRITE_ERROR.with(|rc| unsafe { 334 | let msg = CStr::from_ptr(error_message) 335 | .to_owned() 336 | .into_string() 337 | .unwrap(); 338 | rc.replace(msg); 339 | }); 340 | gg_error_GGE_SUCCESS 341 | } 342 | 343 | #[derive(Debug, Clone, Default)] 344 | pub(crate) struct GGGetSecretValueArgs { 345 | pub ggreq: _gg_request, 346 | pub secret_id: String, 347 | pub version_id: Option, 348 | pub version_stage: Option, 349 | } 350 | 351 | pub extern "C" fn gg_get_secret_value( 352 | ggreq: gg_request, 353 | secret_id: *const ::std::os::raw::c_char, 354 | version_id: *const ::std::os::raw::c_char, 355 | version_stage: *const ::std::os::raw::c_char, 356 | result: *mut gg_request_result, 357 | ) -> gg_error { 358 | unsafe { 359 | GG_GET_SECRET_VALUE_ARGS.with(|rc| { 360 | let rust_version_id = if version_id.is_null() { 361 | None 362 | } else { 363 | Some(CStr::from_ptr(version_id).to_owned().into_string().unwrap()) 364 | }; 365 | 366 | let rust_version_stage = if version_stage.is_null() { 367 | None 368 | } else { 369 | Some( 370 | CStr::from_ptr(version_stage) 371 | .to_owned() 372 | .into_string() 373 | .unwrap(), 374 | ) 375 | }; 376 | let args = GGGetSecretValueArgs { 377 | ggreq: ggreq.as_ref().unwrap().clone(), 378 | secret_id: CStr::from_ptr(secret_id).to_owned().into_string().unwrap(), 379 | version_id: rust_version_id, 380 | version_stage: rust_version_stage, 381 | }; 382 | rc.replace(args); 383 | }); 384 | } 385 | GG_GET_SECRET_VALUE_RETURN.with(|rc| *rc.borrow()) 386 | } 387 | 388 | pub const gg_invoke_type_GG_INVOKE_EVENT: gg_invoke_type = 0; 389 | pub const gg_invoke_type_GG_INVOKE_REQUEST_RESPONSE: gg_invoke_type = 1; 390 | pub const gg_invoke_type_GG_INVOKE_RESERVED_MAX: gg_invoke_type = 2; 391 | pub const gg_invoke_type_GG_INVOKE_RESERVED_PAD: gg_invoke_type = 2147483647; 392 | pub type gg_invoke_type = u32; 393 | pub const gg_runtime_opt_GG_RT_OPT_ASYNC: gg_runtime_opt = 1; 394 | pub const gg_runtime_opt_GG_RT_OPT_RESERVED_PAD: gg_runtime_opt = 2147483647; 395 | pub type gg_runtime_opt = u32; 396 | 397 | #[derive(Debug, Copy, Clone)] 398 | pub struct gg_invoke_options { 399 | pub function_arn: *const ::std::os::raw::c_char, 400 | pub customer_context: *const ::std::os::raw::c_char, 401 | pub qualifier: *const ::std::os::raw::c_char, 402 | pub type_: gg_invoke_type, 403 | pub payload: *const ::std::os::raw::c_void, 404 | pub payload_size: usize, 405 | } 406 | 407 | #[derive(Debug, Default)] 408 | pub(crate) struct GGInvokeArgs { 409 | pub(crate) function_arn: String, 410 | pub(crate) customer_context: Vec, 411 | pub(crate) qualifier: String, 412 | pub(crate) invoke_type: InvokeType, 413 | pub(crate) payload: Vec, 414 | } 415 | 416 | pub extern "C" fn gg_invoke( 417 | ggreq: gg_request, 418 | opts: *const gg_invoke_options, 419 | result: *mut gg_request_result, 420 | ) -> gg_error { 421 | unsafe { 422 | GG_INVOKE_ARGS.with(|rc| { 423 | let mut dst = Vec::with_capacity((*opts).payload_size); 424 | dst.set_len((*opts).payload_size); 425 | std::ptr::copy( 426 | (*opts).payload as *const u8, 427 | dst.as_mut_ptr(), 428 | (*opts).payload_size, 429 | ); 430 | 431 | let args = GGInvokeArgs { 432 | function_arn: CStr::from_ptr((*opts).function_arn) 433 | .to_owned() 434 | .into_string() 435 | .unwrap(), 436 | customer_context: base64::decode( 437 | CStr::from_ptr((*opts).customer_context) 438 | .to_owned() 439 | .into_bytes(), 440 | ) 441 | .unwrap(), 442 | qualifier: CStr::from_ptr((*opts).qualifier) 443 | .to_owned() 444 | .into_string() 445 | .unwrap(), 446 | invoke_type: InvokeType::try_from((*opts).type_).unwrap(), 447 | payload: dst, 448 | }; 449 | rc.replace(args); 450 | }); 451 | } 452 | 453 | gg_error_GGE_SUCCESS 454 | } 455 | 456 | pub extern "C" fn gg_publish_options_init(opts: *mut gg_publish_options) -> gg_error { 457 | GG_PUBLISH_OPTION_INIT_COUNT.with(|rc| { 458 | let new_value = *rc.borrow() + 1; 459 | rc.replace(new_value); 460 | }); 461 | gg_error_GGE_SUCCESS 462 | } 463 | 464 | pub extern "C" fn gg_publish_options_free(opts: gg_publish_options) -> gg_error { 465 | GG_PUBLISH_OPTION_FREE_COUNT.with(|rc| { 466 | let new_value = *rc.borrow() + 1; 467 | rc.replace(new_value); 468 | }); 469 | gg_error_GGE_SUCCESS 470 | } 471 | 472 | pub extern "C" fn gg_publish_options_set_queue_full_policy( 473 | opts: gg_publish_options, 474 | policy: gg_queue_full_policy_options, 475 | ) -> gg_error { 476 | GG_PUBLISH_OPTIONS_SET_QUEUE_FULL_POLICY.with(|rc| { 477 | rc.replace(policy); 478 | }); 479 | gg_error_GGE_SUCCESS 480 | } 481 | 482 | /// Represents arguments passed to gg_publish 483 | #[derive(Debug, Default, PartialEq)] 484 | pub struct GGPublishPayloadArgs { 485 | pub topic: String, 486 | pub payload: Vec, 487 | pub payload_size: usize, 488 | } 489 | 490 | pub extern "C" fn gg_publish_with_options( 491 | ggreq: gg_request, 492 | topic: *const ::std::os::raw::c_char, 493 | payload: *const ::std::os::raw::c_void, 494 | payload_size: usize, 495 | opts: gg_publish_options, 496 | result: *mut gg_request_result, 497 | ) -> gg_error { 498 | unsafe { 499 | GG_PUBLISH_WITH_OPTIONS_ARGS.with(|args| { 500 | // read the void* payload pointer into a byte array 501 | let mut dst = Vec::with_capacity(payload_size); 502 | dst.set_len(payload_size); 503 | std::ptr::copy(payload as *const u8, dst.as_mut_ptr(), payload_size); 504 | 505 | let gg_args = GGPublishPayloadArgs { 506 | topic: CStr::from_ptr(topic).to_owned().into_string().unwrap(), 507 | payload: dst, 508 | payload_size, 509 | }; 510 | 511 | args.replace(gg_args); 512 | }); 513 | } 514 | gg_error_GGE_SUCCESS 515 | } 516 | 517 | pub extern "C" fn gg_publish( 518 | ggreq: gg_request, 519 | topic: *const ::std::os::raw::c_char, 520 | payload: *const ::std::os::raw::c_void, 521 | payload_size: usize, 522 | result: *mut gg_request_result, 523 | ) -> gg_error { 524 | unsafe { 525 | GG_PUBLISH_ARGS.with(|args| { 526 | // read the void* payload pointer into a byte array 527 | let mut dst = Vec::with_capacity(payload_size); 528 | dst.set_len(payload_size); 529 | std::ptr::copy(payload as *const u8, dst.as_mut_ptr(), payload_size); 530 | 531 | let gg_args = GGPublishPayloadArgs { 532 | topic: CStr::from_ptr(topic).to_owned().into_string().unwrap(), 533 | payload: dst, 534 | payload_size, 535 | }; 536 | 537 | args.replace(gg_args); 538 | }); 539 | } 540 | gg_error_GGE_SUCCESS 541 | } 542 | 543 | //noinspection DuplicatedCode 544 | pub extern "C" fn gg_get_thing_shadow( 545 | ggreq: gg_request, 546 | thing_name: *const ::std::os::raw::c_char, 547 | result: *mut gg_request_result, 548 | ) -> gg_error { 549 | unsafe { 550 | GG_SHADOW_THING_ARG.with(|rc| { 551 | let thing_name_rust = CStr::from_ptr(thing_name).to_owned().into_string().unwrap(); 552 | rc.replace(thing_name_rust); 553 | }); 554 | } 555 | gg_error_GGE_SUCCESS 556 | } 557 | 558 | pub extern "C" fn gg_update_thing_shadow( 559 | ggreq: gg_request, 560 | thing_name: *const ::std::os::raw::c_char, 561 | update_payload: *const ::std::os::raw::c_char, 562 | result: *mut gg_request_result, 563 | ) -> gg_error { 564 | unsafe { 565 | GG_UPDATE_PAYLOAD.with(|rc| { 566 | let payload = CStr::from_ptr(update_payload) 567 | .to_owned() 568 | .into_string() 569 | .unwrap(); 570 | rc.replace(payload); 571 | }); 572 | GG_SHADOW_THING_ARG.with(|rc| { 573 | let thing_name_rust = CStr::from_ptr(thing_name).to_owned().into_string().unwrap(); 574 | rc.replace(thing_name_rust); 575 | }); 576 | } 577 | gg_error_GGE_SUCCESS 578 | } 579 | 580 | //noinspection DuplicatedCode 581 | pub extern "C" fn gg_delete_thing_shadow( 582 | ggreq: gg_request, 583 | thing_name: *const ::std::os::raw::c_char, 584 | _result: *mut gg_request_result, 585 | ) -> gg_error { 586 | unsafe { 587 | GG_SHADOW_THING_ARG.with(|rc| { 588 | let thing_name_rust = CStr::from_ptr(thing_name).to_owned().into_string().unwrap(); 589 | rc.replace(thing_name_rust); 590 | }); 591 | } 592 | gg_error_GGE_SUCCESS 593 | } 594 | } 595 | 596 | #[cfg(all(test, not(feature = "coverage")))] 597 | mod bindings_test { 598 | // This is to make sure binding tests are still run 599 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 600 | } 601 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provides error handling 10 | 11 | use crate::bindings::*; 12 | use crate::handler::LambdaContext; 13 | use crate::request::GGRequestResponse; 14 | use crossbeam_channel::{RecvError, SendError}; 15 | use log::error; 16 | use serde_json::Error as SerdeError; 17 | use std::convert::From; 18 | use std::convert::Into; 19 | use std::error::Error; 20 | use std::ffi; 21 | use std::fmt; 22 | use std::io::{Error as IOError, ErrorKind as IOErrorKind}; 23 | use std::string::FromUtf8Error; 24 | 25 | /// Provices a wrapper for the various errors that are incurred both working with the 26 | /// GreenGrass C SDK directly or from the content of the results from it's responses (e.g. http status codes in json response objects) 27 | #[derive(Debug)] 28 | pub enum GGError { 29 | /// Maps to the C API GGE_OUT_OF_MEMORY response 30 | OutOfMemory, 31 | /// Maps to the C API GGE_INVALID_PARAMETER response 32 | InvalidParameter, 33 | /// Maps to the C API GGE_INVALID_STATE response 34 | InvalidState, 35 | /// Maps to the C API GGE_INTERNAL_FAILURE response 36 | InternalFailure, 37 | /// Maps to the C API GGE_TERMINATE response 38 | Terminate, 39 | /// If null pointer from the C API that cannot be recovered from is encountered 40 | NulError(ffi::NulError), 41 | /// C String cannot be coerced into a Rust String 42 | InvalidString(String), 43 | /// If receive an error type from the C API that isn't known 44 | Unknown(String), 45 | /// If there are issues in communicating to the Handler 46 | HandlerChannelSendError(SendError), 47 | /// If there are issues in communicating to the Handler 48 | HandlerChannelRecvError(RecvError), 49 | /// If an AWS response contains an unauthorized error code 50 | Unauthorized(String), 51 | /// Thrown if there is an error with the JSON content we received from AWS 52 | JsonError(SerdeError), 53 | /// When the green grass response is an error 54 | /// If the error is a 404, it should be handled as an Option instead. Otherwise 55 | /// this error type can be returned. 56 | ErrorResponse(GGRequestResponse), 57 | } 58 | 59 | impl GGError { 60 | /// Returns the green grass error as a result. 61 | /// Success code will be Ok(()) 62 | #[allow(non_upper_case_globals)] 63 | pub fn from_code(err_code: gg_error) -> Result<(), GGError> { 64 | match err_code { 65 | gg_error_GGE_SUCCESS => Ok(()), 66 | gg_error_GGE_OUT_OF_MEMORY => Err(Self::OutOfMemory), 67 | gg_error_GGE_INVALID_PARAMETER => Err(Self::InvalidParameter), 68 | gg_error_GGE_INVALID_STATE => Err(Self::InvalidState), 69 | gg_error_GGE_INTERNAL_FAILURE => Err(Self::InternalFailure), 70 | gg_error_GGE_TERMINATE => Err(Self::Terminate), 71 | _ => { 72 | error!("Received unknown error code: {}", err_code); 73 | Err(Self::Unknown(format!("Unknown error code: {}", err_code))) 74 | } 75 | } 76 | } 77 | 78 | // Converts the error to an IoError 79 | #[allow(clippy::wrong_self_convention)] 80 | pub fn as_ioerror(self) -> IOError { 81 | IOError::new(IOErrorKind::Other, self) 82 | } 83 | } 84 | 85 | impl fmt::Display for GGError { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | match self { 88 | Self::OutOfMemory => write!(f, "Process out of memory"), 89 | Self::InvalidParameter => write!(f, "Invalid input Parameter"), 90 | Self::InvalidState => write!(f, "Invalid State"), 91 | Self::InternalFailure => write!(f, "Internal Failure"), 92 | Self::Terminate => write!(f, "Remote signal to terminate received"), 93 | Self::NulError(ref e) => write!(f, "{}", e), 94 | Self::HandlerChannelSendError(ref e) => { 95 | write!(f, "Error sending to handler channel: {}", e) 96 | } 97 | Self::HandlerChannelRecvError(ref e) => { 98 | write!(f, "Error receving from handler channel: {}", e) 99 | } 100 | Self::JsonError(ref e) => write!(f, "Error parsing response: {}", e), 101 | Self::Unknown(ref s) => write!(f, "{}", s), 102 | Self::InvalidString(ref e) => write!(f, "Invalid String: {}", e), 103 | Self::Unauthorized(ref s) => write!(f, "{}", s), 104 | Self::ErrorResponse(ref r) => write!(f, "Green responded with error: {:?}", r), 105 | } 106 | } 107 | } 108 | 109 | impl Error for GGError { 110 | fn source(&self) -> Option<&(dyn Error + 'static)> { 111 | match self { 112 | Self::NulError(ref e) => Some(e), 113 | Self::HandlerChannelSendError(ref e) => Some(e), 114 | Self::HandlerChannelRecvError(ref e) => Some(e), 115 | Self::JsonError(ref e) => Some(e), 116 | _ => None, 117 | } 118 | } 119 | } 120 | 121 | impl From for GGError { 122 | fn from(e: ffi::NulError) -> Self { 123 | GGError::NulError(e) 124 | } 125 | } 126 | 127 | impl From> for GGError { 128 | fn from(e: SendError) -> Self { 129 | GGError::HandlerChannelSendError(e) 130 | } 131 | } 132 | 133 | impl From for GGError { 134 | fn from(e: RecvError) -> Self { 135 | GGError::HandlerChannelRecvError(e) 136 | } 137 | } 138 | 139 | impl Into for GGError { 140 | fn into(self) -> IOError { 141 | self.as_ioerror() 142 | } 143 | } 144 | 145 | impl From for GGError { 146 | fn from(e: FromUtf8Error) -> Self { 147 | Self::InvalidString(format!("{}", e)) 148 | } 149 | } 150 | 151 | impl From for GGError { 152 | fn from(e: SerdeError) -> Self { 153 | Self::JsonError(e) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod test { 159 | use super::*; 160 | use serde_json::Value; 161 | 162 | #[test] 163 | fn test_from_code() { 164 | assert!(GGError::from_code(gg_error_GGE_SUCCESS).is_ok()); 165 | 166 | match GGError::from_code(gg_error_GGE_INTERNAL_FAILURE) { 167 | Err(GGError::InternalFailure) => (), 168 | _ => panic!("Expected InternalFailure"), 169 | }; 170 | 171 | match GGError::from_code(gg_error_GGE_INVALID_PARAMETER) { 172 | Err(GGError::InvalidParameter) => (), 173 | _ => panic!("Expected InvalidParameter"), 174 | }; 175 | 176 | match GGError::from_code(gg_error_GGE_INVALID_STATE) { 177 | Err(GGError::InvalidState) => (), 178 | _ => panic!("Expected InvalidState"), 179 | }; 180 | 181 | match GGError::from_code(gg_error_GGE_TERMINATE) { 182 | Err(GGError::Terminate) => (), 183 | _ => panic!("Expected Terminate"), 184 | }; 185 | 186 | match GGError::from_code(gg_error_GGE_OUT_OF_MEMORY) { 187 | Err(GGError::OutOfMemory) => (), 188 | _ => panic!("Expected InternalFailure"), 189 | }; 190 | 191 | match GGError::from_code(999) { 192 | Err(GGError::Unknown(_)) => (), 193 | _ => panic!("Expected InternalFailure"), 194 | }; 195 | } 196 | 197 | #[test] 198 | fn test_serde_error() { 199 | let result: Result = 200 | serde_json::from_str("sdflkasdf {d92").map_err(GGError::from); 201 | assert!(result.is_err()); 202 | let unwrapped = result.unwrap_err(); 203 | assert!(unwrapped.source().is_some()); 204 | assert!(format!("{}", unwrapped).len() > 10); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provided the handler implementation that can be registered to receive MQTT events 10 | //! 11 | //! # Examples 12 | //! 13 | //! ## Registering a Handler 14 | //! ```rust 15 | //! use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 16 | //! use aws_greengrass_core_rust::runtime::Runtime; 17 | //! use aws_greengrass_core_rust::Initializer; 18 | //! struct MyHandler; 19 | //! impl Handler for MyHandler { 20 | //! fn handle(&self, ctx: LambdaContext) { 21 | //! println!("Received an event! {:?}", ctx); 22 | //! } 23 | //! } 24 | //! 25 | //! let runtime = Runtime::default().with_handler(Some(Box::new(MyHandler))); 26 | //! Initializer::default().with_runtime(runtime).init(); 27 | //! ``` 28 | 29 | /// Provides information around the the event that was received 30 | #[derive(Debug, Clone, PartialEq)] 31 | pub struct LambdaContext { 32 | /// The full lambda ARN 33 | pub function_arn: String, 34 | /// Client context information 35 | pub client_context: String, 36 | /// The message received in bytes 37 | pub message: Vec, 38 | } 39 | 40 | impl LambdaContext { 41 | pub fn new(function_arn: String, client_context: String, message: Vec) -> Self { 42 | LambdaContext { 43 | function_arn, 44 | client_context, 45 | message, 46 | } 47 | } 48 | } 49 | 50 | /// Trait to implement for specifying a handler to the greengrass runtime. 51 | /// This provides the ability to listen to messages sent on MQTT topics.alloc 52 | /// 53 | /// See [`aws_greengrass_core_rust::runtime::Runtime`] on registering handlers. 54 | pub trait Handler { 55 | fn handle(&self, ctx: LambdaContext); 56 | } 57 | 58 | #[cfg(test)] 59 | mod test { 60 | use crate::handler::LambdaContext; 61 | 62 | #[test] 63 | fn test_new() { 64 | let function_arn = "sdlkfjds"; 65 | let client_context = "asdfdfsafdsa"; 66 | let message = "asdjkdsfajl".as_bytes().to_vec(); 67 | let ctx = LambdaContext::new( 68 | function_arn.to_owned(), 69 | client_context.to_owned(), 70 | message.clone(), 71 | ); 72 | assert_eq!(&ctx.function_arn, function_arn); 73 | assert_eq!(&ctx.client_context, client_context); 74 | let cloned = ctx.message.to_owned(); 75 | assert_eq!(cloned, message.clone()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/iotdata.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provides the ability to publish MQTT topics 10 | use log::info; 11 | use serde::ser::Serialize; 12 | use std::convert::TryFrom; 13 | use std::default::Default; 14 | use std::ffi::CString; 15 | use std::os::raw::c_void; 16 | use std::ptr; 17 | 18 | #[cfg(all(test, feature = "mock"))] 19 | use self::mock::*; 20 | 21 | use crate::bindings::*; 22 | use crate::error::GGError; 23 | use crate::request::GGRequestResponse; 24 | use crate::with_request; 25 | use crate::GGResult; 26 | 27 | /// What actions should be taken if an MQTT queue is full 28 | #[derive(Clone, Debug)] 29 | pub enum QueueFullPolicy { 30 | /// GGC will deliver messages to as many targets as possible 31 | BestEffort, 32 | /// GGC will either deliver messages to all targets and return request 33 | /// successful status or deliver to no targets and return a 34 | /// GGError::ErrorResponse with a GGRequestResponse with a status of 'Again' 35 | AllOrError, 36 | } 37 | 38 | impl QueueFullPolicy { 39 | fn to_queue_full_c(&self) -> gg_queue_full_policy_options { 40 | match self { 41 | Self::BestEffort => gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_BEST_EFFORT, 42 | Self::AllOrError => gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_ALL_OR_ERROR, 43 | } 44 | } 45 | } 46 | 47 | /// Options that can be supplied when the client publishes 48 | #[derive(Clone, Debug)] 49 | pub struct PublishOptions { 50 | pub queue_full_policy: QueueFullPolicy, 51 | } 52 | 53 | impl PublishOptions { 54 | /// Define a custom policy when publishing from this client 55 | pub fn with_queue_full_policy(self, queue_full_policy: QueueFullPolicy) -> Self { 56 | PublishOptions { queue_full_policy } 57 | } 58 | } 59 | 60 | impl Default for PublishOptions { 61 | fn default() -> Self { 62 | PublishOptions { 63 | queue_full_policy: QueueFullPolicy::BestEffort, 64 | } 65 | } 66 | } 67 | 68 | /// Provides MQTT publishing to Greengrass lambda functions 69 | /// 70 | /// # Examples 71 | /// 72 | /// ## Basic Publishing 73 | /// ```rust 74 | /// use aws_greengrass_core_rust::iotdata::IOTDataClient; 75 | /// let client = IOTDataClient::default(); 76 | /// if let Err(e) = client.publish("some_topic", r#"{"msg": "some payload"}"#) { 77 | /// eprintln!("An error occurred publishing: {}", e); 78 | /// } 79 | /// ``` 80 | /// 81 | /// ## Publishing with Options 82 | /// ```rust 83 | /// use aws_greengrass_core_rust::iotdata::{PublishOptions, QueueFullPolicy, IOTDataClient}; 84 | /// let options = PublishOptions::default().with_queue_full_policy(QueueFullPolicy::AllOrError); 85 | /// let client = IOTDataClient::default().with_publish_options(Some(options)); 86 | /// if let Err(e) = client.publish("some_topic", r#"{"msg": "some payload"}"#) { 87 | /// eprintln!("An error occurred publishing: {}", e); 88 | /// } 89 | /// ``` 90 | #[derive(Clone)] 91 | pub struct IOTDataClient { 92 | /// The policy that this client will use when publishing 93 | /// if one has been defined 94 | pub publish_options: Option, 95 | /// When the mock feature is turned on this field will contain captured input 96 | /// and values to be returned 97 | #[cfg(all(test, feature = "mock"))] 98 | pub mocks: MockHolder, 99 | } 100 | 101 | impl IOTDataClient { 102 | /// Allows publishing a message of anything that implements AsRef<[u8]> to be published 103 | pub fn publish>(&self, topic: &str, message: T) -> GGResult<()> { 104 | let as_bytes = message.as_ref(); 105 | let size = as_bytes.len(); 106 | self.publish_raw(topic, as_bytes, size) 107 | } 108 | 109 | /// Publish anything that is a deserializable serde object 110 | pub fn publish_json(&self, topic: &str, message: T) -> GGResult<()> { 111 | let bytes = serde_json::to_vec(&message).map_err(GGError::from)?; 112 | self.publish(topic, &bytes) 113 | } 114 | 115 | /// Raw publish method that wraps gg_request_init, gg_publish 116 | #[cfg(not(all(test, feature = "mock")))] 117 | pub fn publish_raw(&self, topic: &str, buffer: &[u8], read: usize) -> GGResult<()> { 118 | self.publish_with_options(topic, buffer, read) 119 | } 120 | 121 | /// This wraps publish_internal and will set any publish options if publish options were specified 122 | /// The primary reason this is a separate function from publish_internal is to ensure that if 123 | /// options is specified we clean up the pointer we create on error 124 | fn publish_with_options(&self, topic: &str, buffer: &[u8], read: usize) -> GGResult<()> { 125 | unsafe { 126 | // If options were defined, initialize the options pointer and 127 | // set queue policy 128 | let options_c: Option = if let Some(po) = &self.publish_options { 129 | let mut opts_c: gg_publish_options = ptr::null_mut(); 130 | let init_resp = gg_publish_options_init(&mut opts_c); 131 | GGError::from_code(init_resp)?; 132 | 133 | let queue_policy_c = po.queue_full_policy.to_queue_full_c(); 134 | let policy_resp = gg_publish_options_set_queue_full_policy(opts_c, queue_policy_c); 135 | if let Err(e) = GGError::from_code(policy_resp) { 136 | // make sure that we free the options pointer 137 | let free_resp = gg_publish_options_free(opts_c); 138 | GGError::from_code(free_resp)?; 139 | return Err(e); 140 | } 141 | 142 | Some(opts_c) 143 | } else { 144 | None 145 | }; 146 | 147 | let publish_result = self.publish_internal(topic, buffer, read, options_c); 148 | 149 | // Clean up the options pointer if we created one 150 | if let Some(opts) = options_c { 151 | let free_resp = gg_publish_options_free(opts); 152 | GGError::from_code(free_resp)?; 153 | } 154 | publish_result 155 | } 156 | } 157 | 158 | /// Raw publish method that wraps gg_request_init, gg_publish 159 | unsafe fn publish_internal( 160 | &self, 161 | topic: &str, 162 | buffer: &[u8], 163 | read: usize, 164 | options_tuple: Option, 165 | ) -> GGResult<()> { 166 | info!("Publishing message of length {} to topic {}", read, topic); 167 | let topic_c = CString::new(topic).map_err(GGError::from)?; 168 | let mut req: gg_request = ptr::null_mut(); 169 | with_request!(req, { 170 | let mut res = gg_request_result { 171 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 172 | }; 173 | let pub_res = if let Some(options_c) = options_tuple { 174 | gg_publish_with_options( 175 | req, 176 | topic_c.as_ptr(), 177 | buffer as *const _ as *const c_void, 178 | read, 179 | options_c, 180 | &mut res, 181 | ) 182 | } else { 183 | gg_publish( 184 | req, 185 | topic_c.as_ptr(), 186 | buffer as *const _ as *const c_void, 187 | read, 188 | &mut res, 189 | ) 190 | }; 191 | GGError::from_code(pub_res)?; 192 | GGRequestResponse::try_from(&res)?.to_error_result(req) 193 | }) 194 | } 195 | 196 | /// Optionally define a publishing options for this Client 197 | #[allow(clippy::needless_update)] 198 | pub fn with_publish_options(self, publish_options: Option) -> Self { 199 | IOTDataClient { 200 | publish_options, 201 | ..self 202 | } 203 | } 204 | 205 | // ----------------------------------- 206 | // Mock methods 207 | // ----------------------------------- 208 | 209 | #[cfg(all(test, feature = "mock"))] 210 | pub fn publish_raw(&self, topic: &str, buffer: &[u8], read: usize) -> GGResult<()> { 211 | log::warn!("Mock publish_raw is being executed!!! This should not happen in prod!!!!"); 212 | self.mocks 213 | .publish_raw_inputs 214 | .borrow_mut() 215 | .push(PublishRawInput(topic.to_owned(), buffer.to_owned(), read)); 216 | // If there is an output return the output 217 | if let Some(output) = self.mocks.publish_raw_outputs.borrow_mut().pop() { 218 | output 219 | } else { 220 | Ok(()) 221 | } 222 | } 223 | 224 | /// When the mock feature is turned on this will contain captured inputs and return 225 | /// provided outputs 226 | #[cfg(all(test, feature = "mock"))] 227 | pub fn with_mocks(self, mocks: MockHolder) -> Self { 228 | IOTDataClient { mocks, ..self } 229 | } 230 | } 231 | 232 | impl Default for IOTDataClient { 233 | fn default() -> Self { 234 | IOTDataClient { 235 | publish_options: None, 236 | #[cfg(all(test, feature = "mock"))] 237 | mocks: MockHolder::default(), 238 | } 239 | } 240 | } 241 | 242 | /// Provides mock testing utilities 243 | #[cfg(all(test, feature = "mock"))] 244 | pub mod mock { 245 | use super::*; 246 | use std::cell::RefCell; 247 | 248 | /// Represents the capture input from the publish_raw field 249 | #[derive(Debug, Clone)] 250 | pub struct PublishRawInput(pub String, pub Vec, pub usize); 251 | 252 | /// Use to override input and output when the mock feature is enabled 253 | #[derive(Debug)] 254 | pub struct MockHolder { 255 | pub publish_raw_inputs: RefCell>, 256 | pub publish_raw_outputs: RefCell>>, 257 | } 258 | 259 | impl MockHolder { 260 | pub fn with_publish_raw_outputs(self, publish_raw_outputs: Vec>) -> Self { 261 | MockHolder { 262 | publish_raw_outputs: RefCell::new(publish_raw_outputs), 263 | ..self 264 | } 265 | } 266 | } 267 | 268 | impl Default for MockHolder { 269 | fn default() -> Self { 270 | MockHolder { 271 | publish_raw_inputs: RefCell::new(vec![]), 272 | publish_raw_outputs: RefCell::new(vec![]), 273 | } 274 | } 275 | } 276 | 277 | // Clone is needed because the contract of IOTDataClient has clone 278 | // We don't necessary clone every field 279 | impl Clone for MockHolder { 280 | fn clone(&self) -> Self { 281 | MockHolder { 282 | publish_raw_inputs: RefCell::new(self.publish_raw_inputs.borrow().clone()), 283 | // NOTE: We can't copy the outputs since result isn't cloneable, so just empty it 284 | publish_raw_outputs: RefCell::new(vec![]), 285 | } 286 | } 287 | } 288 | 289 | // Note: This is to get past compile issues.. Mock testing for threads 290 | // could result in undefined behavior 291 | unsafe impl Send for MockHolder {} 292 | 293 | unsafe impl Sync for MockHolder {} 294 | 295 | #[cfg(test)] 296 | mod test { 297 | use super::*; 298 | 299 | #[test] 300 | fn test_publish_str() { 301 | let topic = "foo"; 302 | let message = "this is my message"; 303 | 304 | let mocks = MockHolder::default().with_publish_raw_outputs(vec![Ok(())]); 305 | let client = IOTDataClient::default().with_mocks(mocks); 306 | let response = client.publish(topic, message).unwrap(); 307 | println!("response: {:?}", response); 308 | 309 | let PublishRawInput(raw_topic, raw_bytes, raw_read) = 310 | &client.mocks.publish_raw_inputs.borrow()[0]; 311 | assert_eq!(raw_topic, topic); 312 | } 313 | } 314 | } 315 | 316 | #[cfg(test)] 317 | mod test { 318 | use super::*; 319 | use serde_json::Value; 320 | 321 | #[cfg(not(feature = "mock"))] 322 | #[test] 323 | fn test_publish_raw() { 324 | reset_test_state(); 325 | let topic = "my_topic"; 326 | let my_payload = b"This is my payload."; 327 | IOTDataClient::default() 328 | .publish_raw(topic, my_payload, my_payload.len()) 329 | .unwrap(); 330 | GG_PUBLISH_ARGS.with(|ref_cell| { 331 | let args = ref_cell.borrow(); 332 | assert_eq!(args.topic, topic); 333 | assert_eq!(args.payload, my_payload); 334 | assert_eq!(args.payload_size, my_payload.len()); 335 | }); 336 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 337 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 338 | } 339 | 340 | #[cfg(not(feature = "mock"))] 341 | #[test] 342 | fn test_publish_with_options() { 343 | reset_test_state(); 344 | let topic = "another topic"; 345 | let my_payload: Value = serde_json::from_str(r#"{"foo": "bar"}"#).unwrap(); 346 | let publish_options = 347 | PublishOptions::default().with_queue_full_policy(QueueFullPolicy::AllOrError); 348 | let client = IOTDataClient::default().with_publish_options(Some(publish_options)); 349 | client.publish_json(topic, my_payload.clone()).unwrap(); 350 | 351 | GG_PUBLISH_WITH_OPTIONS_ARGS.with(|rc| { 352 | let args = rc.borrow(); 353 | let my_payload_as_bytes = serde_json::to_vec(&my_payload).unwrap(); 354 | assert_eq!(args.topic, topic); 355 | assert_eq!(args.payload, my_payload_as_bytes); 356 | assert_eq!(args.payload_size, my_payload_as_bytes.len()); 357 | }); 358 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 359 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 360 | GG_PUBLISH_OPTION_INIT_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 361 | GG_PUBLISH_OPTION_FREE_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 362 | GG_PUBLISH_OPTIONS_SET_QUEUE_FULL_POLICY.with(|rc| { 363 | assert_eq!( 364 | *rc.borrow(), 365 | gg_queue_full_policy_options_GG_QUEUE_FULL_POLICY_ALL_OR_ERROR 366 | ) 367 | }); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/lambda.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | use base64::encode; 10 | use serde::Serialize; 11 | use serde_json; 12 | use std::convert::TryFrom; 13 | use std::default::Default; 14 | use std::ffi::CString; 15 | use std::os::raw::c_void; 16 | use std::ptr; 17 | 18 | use crate::bindings::*; 19 | use crate::error::GGError; 20 | use crate::request::GGRequestResponse; 21 | use crate::with_request; 22 | use crate::GGResult; 23 | 24 | #[cfg(all(test, feature = "mock"))] 25 | use self::mock::*; 26 | 27 | /// Options to invoke a specified lambda 28 | #[derive(Clone, Debug)] 29 | pub struct InvokeOptions { 30 | /// The full ARN of the lambda 31 | pub function_arn: String, 32 | /// base64 json string 33 | pub customer_context: C, 34 | /// Version number of the lambda function 35 | pub qualifier: String, 36 | } 37 | 38 | impl InvokeOptions { 39 | /// Creates a new instance of InvokeOptions 40 | pub fn new(function_arn: String, customer_context: C, qualifier: String) -> Self { 41 | InvokeOptions { 42 | function_arn, 43 | customer_context, 44 | qualifier, 45 | } 46 | } 47 | 48 | fn serialize_customer_context(&self) -> GGResult { 49 | let json = serde_json::to_string(&self.customer_context).map_err(GGError::from)?; 50 | Ok(encode(json)) 51 | } 52 | } 53 | 54 | /// Provides the ability to execute other lambda functions 55 | pub struct LambdaClient { 56 | #[cfg(all(test, feature = "mock"))] 57 | pub mocks: MockHolder, 58 | } 59 | 60 | impl LambdaClient { 61 | /// Allows lambda invocation with an optional payload and wait for a response. 62 | /// 63 | /// # Example 64 | /// ```rust 65 | /// use serde::Serialize; 66 | /// use aws_greengrass_core_rust::lambda::LambdaClient; 67 | /// use aws_greengrass_core_rust::lambda::InvokeOptions; 68 | /// 69 | /// #[derive(Serialize)] 70 | /// struct Context { 71 | /// foo: String, 72 | /// bar: String, 73 | /// } 74 | /// 75 | /// let payload = "Some payload";; 76 | /// let context = Context { foo: "blah".to_owned(), bar: "baz".to_owned() }; 77 | /// let options = InvokeOptions::new("my_func_arn".to_owned(), context, "lambda qualifier".to_owned()); 78 | /// let response = LambdaClient::default().invoke_sync(options, Some(payload)); 79 | /// println!("response: {:?}", response); 80 | /// ``` 81 | #[cfg(not(feature = "mock"))] 82 | pub fn invoke_sync>( 83 | &self, 84 | option: InvokeOptions, 85 | payload: Option

, 86 | ) -> GGResult>> { 87 | invoke(&option, InvokeType::InvokeRequestResponse, &payload) 88 | } 89 | 90 | /// Allows lambda invocation with an optional payload. The lambda will be executed asynchronously and no response will be returned 91 | /// 92 | /// # Example 93 | /// ```rust 94 | /// use serde::Serialize; 95 | /// use aws_greengrass_core_rust::lambda::LambdaClient; 96 | /// use aws_greengrass_core_rust::lambda::InvokeOptions; 97 | /// 98 | /// #[derive(Serialize)] 99 | /// struct Context { 100 | /// foo: String, 101 | /// bar: String, 102 | /// } 103 | /// 104 | /// let payload = "Some payload"; 105 | /// let context = Context { foo: "blah".to_owned(), bar: "baz".to_owned() }; 106 | /// let options = InvokeOptions::new("my_func_arn".to_owned(), context, "lambda qualifier".to_owned()); 107 | /// if let Err(e) = LambdaClient::default().invoke_async(options, Some(payload)) { 108 | /// eprintln!("Error occurred: {}", e); 109 | /// } 110 | /// ``` 111 | #[cfg(not(feature = "mock"))] 112 | pub fn invoke_async>( 113 | &self, 114 | option: InvokeOptions, 115 | payload: Option

, 116 | ) -> GGResult<()> { 117 | invoke(&option, InvokeType::InvokeEvent, &payload).map(|_| ()) 118 | } 119 | 120 | /// Allows lambda functions that have been invoked by another lambda to send a response back 121 | /// On success send Ok(P) 122 | /// On Error send Err(String) 123 | #[cfg(not(feature = "mock"))] 124 | pub fn send_response(&self, result: Result<&[u8], &str>) -> GGResult<()> { 125 | unsafe { 126 | match result { 127 | Ok(bytes) => write_lambda_response(bytes), 128 | Err(e) => write_lambda_err_response(e), 129 | } 130 | } 131 | } 132 | 133 | // ----------------------------------- 134 | // Mock methods 135 | // ----------------------------------- 136 | 137 | #[cfg(all(test, feature = "mock"))] 138 | pub fn invoke_sync>( 139 | &self, 140 | option: &InvokeOptions, 141 | payload: &Option

, 142 | ) -> GGResult>> { 143 | log::warn!("Mock invoke_sync is being executed!!! This should not happen in prod!!!!"); 144 | let opts = InvokeOptionsInput::from(option); 145 | let payload_bytes = payload.as_ref().map(|p| p.as_ref().to_vec()); 146 | self.mocks 147 | .invoke_sync_inputs 148 | .borrow_mut() 149 | .push(InvokeInput(opts, payload_bytes)); 150 | 151 | if let Some(output) = self.mocks.invoke_sync_outputs.borrow_mut().pop() { 152 | output.map(|vec| serde_json::from_slice(vec.as_ref()).unwrap()) 153 | } else { 154 | Ok(None) 155 | } 156 | } 157 | 158 | #[cfg(all(test, feature = "mock"))] 159 | pub fn invoke_async>( 160 | &self, 161 | option: &InvokeOptions, 162 | payload: &Option

, 163 | ) -> GGResult<()> { 164 | log::warn!("Mock invoke_async is being executed!!! This should not happen in prod!!!!"); 165 | let opts = InvokeOptionsInput::from(option); 166 | 167 | let payload_bytes = payload.as_ref().map(|p| p.as_ref().to_vec()); 168 | self.mocks 169 | .invoke_async_inputs 170 | .borrow_mut() 171 | .push(InvokeInput(opts, payload_bytes)); 172 | 173 | if let Some(output) = self.mocks.invoke_async_outputs.borrow_mut().pop() { 174 | output 175 | } else { 176 | Ok(()) 177 | } 178 | } 179 | 180 | #[cfg(all(test, feature = "mock"))] 181 | pub fn send_response(&self, result: Result<&[u8], &str>) -> GGResult<()> { 182 | log::warn!("Mock send_response is being executed!!! This should not happen in prod!!!!"); 183 | let r = result.map(|a| a.to_vec()).map_err(|s| s.to_owned()); 184 | self.mocks.send_response_inputs.borrow_mut().push(r); 185 | if let Some(output) = self.mocks.send_response_outputs.borrow_mut().pop() { 186 | output 187 | } else { 188 | Ok(()) 189 | } 190 | } 191 | 192 | /// When the mock feature is turned on this will contain captured inputs and return 193 | /// provided outputs 194 | #[cfg(all(test, feature = "mock"))] 195 | pub fn with_mocks(self, mocks: MockHolder) -> Self { 196 | LambdaClient { mocks, ..self } 197 | } 198 | } 199 | 200 | impl Default for LambdaClient { 201 | fn default() -> Self { 202 | LambdaClient { 203 | #[cfg(all(test, feature = "mock"))] 204 | mocks: MockHolder::default(), 205 | } 206 | } 207 | } 208 | 209 | #[derive(Clone, Debug, PartialEq)] 210 | pub(crate) enum InvokeType { 211 | /// Invoke the function asynchronously 212 | InvokeEvent, 213 | /// Invoke the function synchronously (default) 214 | InvokeRequestResponse, 215 | } 216 | 217 | impl TryFrom for InvokeType { 218 | type Error = GGError; 219 | 220 | #[allow(non_upper_case_globals)] 221 | fn try_from(value: gg_invoke_type) -> Result { 222 | match value { 223 | gg_invoke_type_GG_INVOKE_EVENT => Ok(Self::InvokeEvent), 224 | gg_invoke_type_GG_INVOKE_REQUEST_RESPONSE => Ok(Self::InvokeRequestResponse), 225 | _ => Err(GGError::Unknown(format!("Unknown invoke type: {}", value))), 226 | } 227 | } 228 | } 229 | 230 | impl Default for InvokeType { 231 | fn default() -> Self { 232 | InvokeType::InvokeEvent 233 | } 234 | } 235 | 236 | impl InvokeType { 237 | fn as_c_invoke_type(&self) -> gg_invoke_type { 238 | match *self { 239 | Self::InvokeEvent => gg_invoke_type_GG_INVOKE_EVENT, 240 | Self::InvokeRequestResponse => gg_invoke_type_GG_INVOKE_REQUEST_RESPONSE, 241 | } 242 | } 243 | } 244 | 245 | fn invoke>( 246 | option: &InvokeOptions, 247 | invoke_type: InvokeType, 248 | payload: &Option

, 249 | ) -> GGResult>> { 250 | unsafe { 251 | let function_arn_c = CString::new(option.function_arn.as_str()).map_err(GGError::from)?; 252 | let customer_context_c = 253 | CString::new(option.serialize_customer_context()?).map_err(GGError::from)?; 254 | let qualifier_c = CString::new(option.qualifier.as_str()).map_err(GGError::from)?; 255 | let payload_bytes = payload.as_ref().map(|p| p.as_ref()); 256 | let (payload_c, payload_size) = if let Some(p) = payload_bytes { 257 | (p as *const _ as *const c_void, p.len()) 258 | } else { 259 | (ptr::null(), 0) 260 | }; 261 | 262 | let options_c = Box::new(gg_invoke_options { 263 | function_arn: function_arn_c.as_ptr(), 264 | customer_context: customer_context_c.as_ptr(), 265 | qualifier: qualifier_c.as_ptr(), 266 | type_: invoke_type.as_c_invoke_type(), 267 | payload: payload_c, 268 | payload_size, 269 | }); 270 | 271 | let mut req: gg_request = ptr::null_mut(); 272 | with_request!(req, { 273 | let mut res = gg_request_result { 274 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 275 | }; 276 | let raw_options = Box::into_raw(options_c); 277 | let invoke_res = gg_invoke(req, raw_options, &mut res); 278 | let _ = Box::from_raw(raw_options); // cleanup 279 | GGError::from_code(invoke_res)?; 280 | 281 | match invoke_type { 282 | InvokeType::InvokeEvent => { 283 | GGRequestResponse::try_from(&res)?.to_error_result(req)?; 284 | Ok(None) 285 | } 286 | InvokeType::InvokeRequestResponse => GGRequestResponse::try_from(&res)?.read(req), 287 | } 288 | }) 289 | } 290 | } 291 | 292 | unsafe fn write_lambda_response(buffer: &[u8]) -> GGResult<()> { 293 | let buffer_c = buffer as *const _ as *const c_void; 294 | let resp = gg_lambda_handler_write_response(buffer_c, buffer.len()); 295 | GGError::from_code(resp) 296 | } 297 | 298 | unsafe fn write_lambda_err_response(err_msg: &str) -> GGResult<()> { 299 | let err_msg_c = CString::new(err_msg).map_err(GGError::from)?; 300 | let resp = gg_lambda_handler_write_error(err_msg_c.as_ptr()); 301 | GGError::from_code(resp) 302 | } 303 | 304 | /// Provides mock testing utilities 305 | #[cfg(all(test, feature = "mock"))] 306 | pub mod mock { 307 | use super::*; 308 | use std::cell::RefCell; 309 | 310 | #[derive(Clone)] 311 | pub struct InvokeOptionsInput { 312 | /// The full ARN of the lambda 313 | pub function_arn: String, 314 | /// base64 json string 315 | pub customer_context: Vec, 316 | /// Version number of the lambda function 317 | pub qualifier: String, 318 | } 319 | 320 | impl From<&InvokeOptions> for InvokeOptionsInput { 321 | fn from(opts: &InvokeOptions) -> Self { 322 | InvokeOptionsInput { 323 | function_arn: opts.function_arn.to_owned(), 324 | customer_context: serde_json::to_vec(&opts.customer_context).unwrap(), 325 | qualifier: opts.qualifier.to_owned(), 326 | } 327 | } 328 | } 329 | 330 | #[derive(Clone)] 331 | pub struct InvokeInput(pub InvokeOptionsInput, pub Option>); 332 | 333 | /// used to override input and output when the mock feature is enabled 334 | pub struct MockHolder { 335 | pub invoke_sync_inputs: RefCell>, 336 | pub invoke_sync_outputs: RefCell>>>, 337 | pub invoke_async_inputs: RefCell>, 338 | pub invoke_async_outputs: RefCell>>, 339 | pub send_response_inputs: RefCell, String>>>, 340 | pub send_response_outputs: RefCell>>, 341 | } 342 | 343 | impl MockHolder { 344 | pub fn with_invoke_sync_outputs(self, invoke_sync_outputs: Vec>>) -> Self { 345 | MockHolder { 346 | invoke_sync_outputs: RefCell::new(invoke_sync_outputs), 347 | ..self 348 | } 349 | } 350 | 351 | pub fn with_invoke_async_outputs(self, invoke_async_outputs: Vec>) -> Self { 352 | MockHolder { 353 | invoke_async_outputs: RefCell::new(invoke_async_outputs), 354 | ..self 355 | } 356 | } 357 | } 358 | 359 | impl Default for MockHolder { 360 | fn default() -> Self { 361 | MockHolder { 362 | invoke_sync_inputs: RefCell::new(vec![]), 363 | invoke_sync_outputs: RefCell::new(vec![]), 364 | invoke_async_inputs: RefCell::new(vec![]), 365 | invoke_async_outputs: RefCell::new(vec![]), 366 | send_response_inputs: RefCell::new(vec![]), 367 | send_response_outputs: RefCell::new(vec![]), 368 | } 369 | } 370 | } 371 | 372 | impl Clone for MockHolder { 373 | fn clone(&self) -> Self { 374 | MockHolder { 375 | invoke_sync_inputs: RefCell::new(self.invoke_sync_inputs.borrow().clone()), 376 | invoke_async_inputs: RefCell::new(self.invoke_async_inputs.borrow().clone()), 377 | send_response_inputs: RefCell::new(self.send_response_inputs.borrow().clone()), 378 | invoke_sync_outputs: RefCell::new(vec![]), 379 | invoke_async_outputs: RefCell::new(vec![]), 380 | send_response_outputs: RefCell::new(vec![]), 381 | } 382 | } 383 | } 384 | 385 | // Note: This is to get past compile issues.. Mock testing for threads 386 | // could result in undefined behavior 387 | unsafe impl Send for MockHolder {} 388 | 389 | unsafe impl Sync for MockHolder {} 390 | } 391 | 392 | #[cfg(test)] 393 | mod test { 394 | use super::*; 395 | use serde::Deserialize; 396 | 397 | #[derive(Serialize, Deserialize, Clone)] 398 | struct TestContext { 399 | foo: String, 400 | } 401 | 402 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 403 | struct TestPayload { 404 | msg: String, 405 | } 406 | 407 | //noinspection DuplicatedCode 408 | #[cfg(not(feature = "mock"))] 409 | #[test] 410 | fn test_invoke_async() { 411 | reset_test_state(); 412 | 413 | let function_arn = "function_arn2323"; 414 | 415 | let context = TestContext { 416 | foo: "bar".to_string(), 417 | }; 418 | 419 | let payload = TestPayload { 420 | msg: "The message of my payload".to_owned(), 421 | }; 422 | 423 | let qualifier = "12121221"; 424 | 425 | let payload_bytes = serde_json::to_vec(&payload).unwrap(); 426 | 427 | let options = InvokeOptions::new( 428 | function_arn.to_owned(), 429 | context.clone(), 430 | qualifier.to_owned(), 431 | ); 432 | 433 | LambdaClient::default() 434 | .invoke_async(options, Some(payload_bytes.clone())) 435 | .unwrap(); 436 | 437 | GG_INVOKE_ARGS.with(|rc| { 438 | let args = rc.borrow(); 439 | assert_eq!(args.qualifier, qualifier); 440 | assert_eq!(args.payload, payload_bytes); 441 | assert_eq!(args.customer_context, serde_json::to_vec(&context).unwrap()); 442 | assert_eq!(args.function_arn, function_arn); 443 | assert_eq!(args.invoke_type, InvokeType::InvokeEvent); 444 | }); 445 | 446 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 447 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 448 | } 449 | 450 | //noinspection DuplicatedCode 451 | #[cfg(not(feature = "mock"))] 452 | #[test] 453 | fn test_invoke_sync() { 454 | reset_test_state(); 455 | let response = TestPayload { 456 | msg: "This is the sync response!".to_owned(), 457 | }; 458 | GG_REQUEST_READ_BUFFER.with(|rc| { 459 | let bytes = serde_json::to_vec(&response).unwrap(); 460 | rc.replace(bytes); 461 | }); 462 | 463 | let function_arn = "function_arn2323t867"; 464 | 465 | let context = TestContext { 466 | foo: "bark".to_string(), 467 | }; 468 | 469 | let payload = TestPayload { 470 | msg: "The message of my payloadkhkjb".to_owned(), 471 | }; 472 | 473 | let qualifier = "12121221"; 474 | 475 | let payload_bytes = serde_json::to_vec(&payload).unwrap(); 476 | 477 | let options = InvokeOptions::new( 478 | function_arn.to_owned(), 479 | context.clone(), 480 | qualifier.to_owned(), 481 | ); 482 | 483 | let result: Vec = LambdaClient::default() 484 | .invoke_sync(options, Some(payload_bytes.clone())) 485 | .unwrap() 486 | .unwrap(); 487 | assert_eq!( 488 | serde_json::from_slice::(result.as_ref()).unwrap(), 489 | response 490 | ); 491 | 492 | GG_INVOKE_ARGS.with(|rc| { 493 | let args = rc.borrow(); 494 | assert_eq!(args.qualifier, qualifier); 495 | assert_eq!(args.payload, payload_bytes); 496 | assert_eq!(args.customer_context, serde_json::to_vec(&context).unwrap()); 497 | assert_eq!(args.function_arn, function_arn); 498 | assert_eq!(args.invoke_type, InvokeType::InvokeRequestResponse); 499 | }); 500 | 501 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 502 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 503 | } 504 | 505 | #[test] 506 | #[cfg(not(feature = "mock"))] 507 | fn test_send_response() { 508 | let my_succesful_response = b"response is here"; 509 | LambdaClient::default() 510 | .send_response(Ok(my_succesful_response)) 511 | .unwrap(); 512 | GG_LAMBDA_HANDLER_WRITE_RESPONSE.with(|rc| { 513 | assert_eq!(*rc.borrow(), my_succesful_response); 514 | }); 515 | } 516 | 517 | #[test] 518 | #[cfg(not(feature = "mock"))] 519 | fn test_send_err_response() { 520 | let my_err_response = "error response is here"; 521 | LambdaClient::default() 522 | .send_response(Err(my_err_response)) 523 | .unwrap(); 524 | GG_LAMBDA_HANDLER_WRITE_ERROR.with(|rc| { 525 | assert_eq!(*rc.borrow(), my_err_response); 526 | }); 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provices an idiomatic Rust API on top of the AWS GreenGrass Core C SDK 10 | //! 11 | //! # Quick Start 12 | //! ```no_run 13 | //! use aws_greengrass_core_rust::Initializer; 14 | //! use aws_greengrass_core_rust::log as gglog; 15 | //! use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 16 | //! use log::{info, error, LevelFilter}; 17 | //! use aws_greengrass_core_rust::runtime::Runtime; 18 | //! 19 | //! struct HelloHandler; 20 | //! 21 | //! impl Handler for HelloHandler { 22 | //! fn handle(&self, ctx: LambdaContext) { 23 | //! info!("Received context: {:#?}", ctx); 24 | //! let msg = String::from_utf8(ctx.message).expect("Message was not a valid utf8 string"); 25 | //! info!("Received event: {}", msg); 26 | //! } 27 | //! } 28 | //! 29 | //! gglog::init_log(LevelFilter::Info); 30 | //! let runtime = Runtime::default().with_handler(Some(Box::new(HelloHandler))); 31 | //! if let Err(e) = Initializer::default().with_runtime(runtime).init() { 32 | //! error!("Initialization failed: {}", e); 33 | //! std::process::exit(1); 34 | //! } 35 | //! ``` 36 | #![allow(unused_unsafe)] // because the test bindings will complain otherwise 37 | 38 | mod bindings; 39 | pub mod error; 40 | pub mod handler; 41 | pub mod iotdata; 42 | pub mod lambda; 43 | pub mod log; 44 | pub mod request; 45 | pub mod runtime; 46 | pub mod secret; 47 | pub mod shadow; 48 | 49 | use crate::bindings::gg_global_init; 50 | use crate::error::GGError; 51 | use crate::runtime::Runtime; 52 | use std::default::Default; 53 | 54 | pub type GGResult = Result; 55 | 56 | /// Provides the ability initialize the greengrass runtime 57 | pub struct Initializer { 58 | runtime: Runtime, 59 | } 60 | 61 | impl Initializer { 62 | pub fn init(self) -> GGResult<()> { 63 | unsafe { 64 | // At this time there are no options for gg_global_init 65 | let init_res = gg_global_init(0); 66 | GGError::from_code(init_res)?; 67 | self.runtime.start()?; 68 | } 69 | Ok(()) 70 | } 71 | 72 | /// Initialize the greengrass with the specified runtime object. 73 | /// 74 | /// This must be called if you want to provide a Runtime with a [`handler::Handler`]. 75 | /// 76 | /// ```edition2018 77 | /// use aws_greengrass_core_rust::runtime::Runtime; 78 | /// use aws_greengrass_core_rust::Initializer; 79 | /// 80 | /// Initializer::default().with_runtime(Runtime::default()); 81 | /// ``` 82 | pub fn with_runtime(self, runtime: Runtime) -> Self { 83 | Initializer { runtime } 84 | } 85 | } 86 | 87 | /// Creates a Initializer with the default Runtime 88 | impl Default for Initializer { 89 | fn default() -> Self { 90 | Initializer { 91 | runtime: Runtime::default(), 92 | } 93 | } 94 | } 95 | 96 | /// Initialize the Greengrass runtime without a handler 97 | pub fn init() -> GGResult<()> { 98 | Initializer::default().init() 99 | } 100 | 101 | #[cfg(test)] 102 | pub mod test { 103 | use std::cell::{Ref, RefCell}; 104 | 105 | /// Provides a mechanism that can be used to save calls from a Mock implementation 106 | /// ```rust 107 | /// use aws_greengrass_core_rust::test::CallHolder; 108 | /// use std::rc::Rc; 109 | /// 110 | /// trait MyTrait { 111 | /// fn call(&self, foo: &str); 112 | /// } 113 | /// 114 | /// struct MockImpl { 115 | /// call_holder: Rc> 116 | /// } 117 | /// 118 | /// impl MockTrait for MockImpl { 119 | /// fn call(&self, foo: &str) { 120 | /// self.call_holder.push(foo.to_owned()); 121 | /// } 122 | /// } 123 | /// ``` 124 | pub struct CallHolder { 125 | calls: RefCell>, 126 | } 127 | 128 | impl CallHolder { 129 | pub fn new() -> Self { 130 | CallHolder { 131 | calls: RefCell::new(Vec::::new()), 132 | } 133 | } 134 | 135 | /// Push new call information to the internal RefCell 136 | pub fn push(&self, call: T) { 137 | self.calls.borrow_mut().push(call) 138 | } 139 | 140 | /// Return all the calls made 141 | pub fn calls(&self) -> Ref> { 142 | self.calls.borrow() 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provide a log crate log implementation that delegates to the the Greengrass logging infrastructure 10 | use crate::bindings::*; 11 | use lazy_static::lazy_static; 12 | use log::{self, Level, LevelFilter, Log, Metadata, Record}; 13 | use std::ffi::CString; 14 | 15 | lazy_static! { 16 | static ref LOGGER: GGLogger = GGLogger; 17 | } 18 | 19 | /// A logger implementation that wraps the greengrass logging backend 20 | #[derive(Default)] 21 | struct GGLogger; 22 | 23 | impl Log for GGLogger { 24 | fn enabled(&self, _: &Metadata) -> bool { 25 | true 26 | } 27 | 28 | fn log(&self, record: &Record) { 29 | if self.enabled(record.metadata()) { 30 | to_gg_log(record) 31 | } 32 | } 33 | 34 | fn flush(&self) {} 35 | } 36 | 37 | /// Initializes the Greengrass Logger with the specified run level 38 | /// 39 | /// # Examples 40 | /// ```example2018 41 | /// use log::LogLevel; 42 | /// use aws_greengrass_core_rust::log as gglog; 43 | /// 44 | /// gglog::init_log(Level::Debug); 45 | /// ``` 46 | pub fn init_log(max_level: LevelFilter) { 47 | log::set_max_level(max_level); 48 | log::set_logger(&*LOGGER).expect("GGLogger implementation could not be set as logger"); 49 | } 50 | 51 | /// Converts a [`log::Record`] to a c log entry and sends it to gg_log 52 | fn to_gg_log(record: &Record) { 53 | let formatted = format!("{} -- {}", record.target(), record.args()); 54 | let bytes = formatted.into_bytes(); 55 | 56 | let c_to_print = CString::new(bytes.as_slice()).expect("CString: new failed"); 57 | let level = to_gg_log_level(record.level()); 58 | unsafe { 59 | gg_log(level, c_to_print.as_ptr()); 60 | } 61 | } 62 | 63 | /// Coerces a [`log::Level`] into a green grass log level 64 | fn to_gg_log_level(l: Level) -> gg_log_level { 65 | match l { 66 | Level::Info => gg_log_level_GG_LOG_INFO, 67 | Level::Warn => gg_log_level_GG_LOG_WARN, 68 | Level::Error => gg_log_level_GG_LOG_ERROR, 69 | _ => gg_log_level_GG_LOG_DEBUG, 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod test { 75 | use super::*; 76 | 77 | #[cfg(not(feature = "mock"))] 78 | #[test] 79 | fn test_log() { 80 | init_log(LevelFilter::Trace); 81 | log::info!("info test"); 82 | log::warn!("warn test"); 83 | log::error!("error test"); 84 | log::debug!("debug test"); 85 | log::trace!("trace test"); // trace should end up as debug 86 | GG_LOG_ARGS.with(|rc| { 87 | let borrowed = rc.borrow(); 88 | assert_eq!(borrowed.len(), 5); 89 | let info_value = LogArgs::new( 90 | gg_log_level_GG_LOG_INFO, 91 | "aws_greengrass_core_rust::log::test -- info test", 92 | ); 93 | assert!(borrowed.contains(&info_value)); 94 | let debug_value = LogArgs::new( 95 | gg_log_level_GG_LOG_DEBUG, 96 | "aws_greengrass_core_rust::log::test -- debug test", 97 | ); 98 | assert!(borrowed.contains(&debug_value)); 99 | let warn_value = LogArgs::new( 100 | gg_log_level_GG_LOG_WARN, 101 | "aws_greengrass_core_rust::log::test -- warn test", 102 | ); 103 | assert!(borrowed.contains(&warn_value)); 104 | let error_value = LogArgs::new( 105 | gg_log_level_GG_LOG_ERROR, 106 | "aws_greengrass_core_rust::log::test -- error test", 107 | ); 108 | assert!(borrowed.contains(&error_value)); 109 | let trace_value = LogArgs::new( 110 | gg_log_level_GG_LOG_DEBUG, 111 | "aws_greengrass_core_rust::log::test -- trace test", 112 | ); 113 | assert!(borrowed.contains(&trace_value)); 114 | }); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provides utilities for working with handling green grass response objects. 10 | //! Generally consumers of the API will not use items in this module, except in error cases. 11 | //! In error cases the GGRequestResponse struct will be embedded in the the GGError::ErrorResponse. 12 | //! In this case we are exposing it as I we don't know every possible error that AWS returns. 13 | //! 14 | //! # Examples 15 | //! 16 | //! ## Error Handling Example 17 | //! ```rust 18 | //! use aws_greengrass_core_rust::iotdata::IOTDataClient; 19 | //! use aws_greengrass_core_rust::error::GGError; 20 | //! use aws_greengrass_core_rust::request::GGRequestStatus; 21 | //! match IOTDataClient::default().publish("my topic", "my payload") { 22 | //! Ok(_) => println!("Yay, it worked!"), 23 | //! Err(GGError::ErrorResponse(resp)) => { 24 | //! match resp.request_status { 25 | //! GGRequestStatus::Again => eprintln!("You should retry again because you were throttled"), 26 | //! _ => eprintln!("An error that is probably unrecoverable happened."), 27 | //! } 28 | //! } 29 | //! _ => eprintln!("Another greengrass system error occurred"), 30 | //! } 31 | //! ``` 32 | use crate::bindings::*; 33 | use crate::error::GGError; 34 | use crate::GGResult; 35 | use log::{error, warn}; 36 | use serde::{Deserialize, Serialize}; 37 | use std::convert::TryFrom; 38 | use std::default::Default; 39 | use std::ffi::c_void; 40 | 41 | /// The size of buffer we will use when reading results 42 | /// from the C API 43 | const BUFFER_SIZE: usize = 512; 44 | 45 | /// Greengrass SDK request status enum 46 | /// Maps to gg_request_status 47 | #[derive(Debug, Clone, PartialEq, Serialize)] 48 | pub enum GGRequestStatus { 49 | /// function call returns expected payload type 50 | Success, 51 | /// function call is successfull, however lambda responded with an error 52 | Handled, 53 | /// function call is unsuccessfull, lambda exits abnormally 54 | Unhandled, 55 | /// System encounters unknown error. Check logs for more details 56 | Unknown, 57 | /// function call is throttled, try again 58 | Again, 59 | } 60 | 61 | impl TryFrom for GGRequestStatus { 62 | type Error = GGError; 63 | 64 | #[allow(non_upper_case_globals)] 65 | fn try_from(value: gg_request_status) -> Result { 66 | match value { 67 | gg_request_status_GG_REQUEST_SUCCESS => Ok(Self::Success), 68 | gg_request_status_GG_REQUEST_HANDLED => Ok(Self::Handled), 69 | gg_request_status_GG_REQUEST_UNHANDLED => Ok(Self::Unhandled), 70 | gg_request_status_GG_REQUEST_UNKNOWN => Ok(Self::Unknown), 71 | gg_request_status_GG_REQUEST_AGAIN => Ok(Self::Again), 72 | _ => Err(Self::Error::Unknown(format!( 73 | "Unknown error code: {}", 74 | value 75 | ))), 76 | } 77 | } 78 | } 79 | 80 | /// Represents the response returned by greengrass via the gg_read_response C function. 81 | /// Generally this will only be used internally to the API (Success), except in the case of most error responses. 82 | /// In most server side error responses (where we receive a json error object) this object will be contained 83 | /// inside the GGError::ErrorResponse error. 84 | #[derive(Debug, Clone, Serialize)] 85 | pub struct GGRequestResponse { 86 | /// The status of the GG request 87 | pub request_status: GGRequestStatus, 88 | /// If there was an error response, what was it. 89 | pub error_response: Option, 90 | } 91 | 92 | impl GGRequestResponse { 93 | pub fn with_error_response(self, error_response: Option) -> Self { 94 | GGRequestResponse { 95 | error_response, 96 | ..self 97 | } 98 | } 99 | 100 | /// Returns true if the request status is anything other than GGRequestStatus::Success 101 | pub fn is_error(&self) -> bool { 102 | self.request_status != GGRequestStatus::Success 103 | } 104 | 105 | /// Ok(()) if there is no error, otherwise the error we found 106 | /// This is useful for requests that do not contain a body 107 | pub(crate) fn to_error_result(&self, req: gg_request) -> GGResult<()> { 108 | match self.determine_error(req) { 109 | ErrorState::Error(e) => Err(e), 110 | _ => Ok(()), // Ignore the NotFoundError too 111 | } 112 | } 113 | 114 | /// Attempt to read the response body. 115 | /// If the response is an error the error will be returned else the body in bytes. 116 | /// This is useful for requests that contain a body 117 | pub(crate) fn read(&self, req: gg_request) -> GGResult>> { 118 | match self.determine_error(req) { 119 | ErrorState::None => { 120 | let data = read_response_data(req)?; 121 | Ok(Some(data)) 122 | } 123 | ErrorState::NotFoundError => Ok(None), 124 | ErrorState::Error(e) => Err(e), 125 | } 126 | } 127 | 128 | /// If the response is an error, return it as Some(GGError) 129 | /// None if it isn't an error 130 | fn determine_error(&self, req: gg_request) -> ErrorState { 131 | // If we know there isn't an error, return 132 | if !self.is_error() { 133 | return ErrorState::None; 134 | } 135 | 136 | // If this is an error than try to read the response body 137 | // So we can see what kind of error it is 138 | let response_data = match read_response_data(req) { 139 | // if the error response is empty we could have an UNKNOWN response 140 | // which might not be an error at all. 141 | // This best we can do is log 142 | Ok(data) if data.is_empty() => { 143 | warn!( 144 | "Could not find an error response for non Success request of {:?}", 145 | self.request_status 146 | ); 147 | return ErrorState::None; 148 | } 149 | Ok(data) => data, 150 | Err(e) => { 151 | error!( 152 | "An error occurred attempting to read error response data: {}", 153 | e 154 | ); 155 | return ErrorState::Error(e); 156 | } 157 | }; 158 | 159 | let err_resp = match ErrorResponse::try_from(response_data.as_slice()) { 160 | Ok(resp) => resp, 161 | // A parsing error occurred 162 | Err(e) => return ErrorState::Error(e), 163 | }; 164 | 165 | match err_resp.code { 166 | 404 => ErrorState::NotFoundError, 167 | 401 => ErrorState::Error(GGError::Unauthorized(err_resp.message)), 168 | _ => ErrorState::Error(GGError::ErrorResponse(self.clone())), 169 | } 170 | } 171 | } 172 | 173 | /// There are three states instead of two (why I didn't use option). 174 | /// This is because I want to capture the 404 error response and wrap it as Option in an attempt 175 | /// to make the API feel more idiomatic 176 | enum ErrorState { 177 | /// All other errors 178 | Error(GGError), 179 | /// A 404 error was returned 180 | NotFoundError, 181 | /// No Error response was found 182 | None, 183 | } 184 | 185 | impl Default for GGRequestResponse { 186 | fn default() -> Self { 187 | GGRequestResponse { 188 | request_status: GGRequestStatus::Success, 189 | error_response: None, 190 | } 191 | } 192 | } 193 | 194 | impl TryFrom<&gg_request_result> for GGRequestResponse { 195 | type Error = GGError; 196 | 197 | fn try_from(value: &gg_request_result) -> Result { 198 | let status = GGRequestStatus::try_from(value.request_status)?; 199 | Ok(GGRequestResponse { 200 | request_status: status, 201 | error_response: None, 202 | }) 203 | } 204 | } 205 | 206 | /// Represents an Error Response from greengrass JSON object 207 | #[derive(Debug, Clone, Deserialize, Serialize)] 208 | pub struct ErrorResponse { 209 | /// AWS uses the HTTP status codes even though this is over MQTT 210 | pub code: u16, 211 | /// Message related to the error 212 | pub message: String, 213 | /// The time this error occurred 214 | pub timestamp: u64, 215 | } 216 | 217 | impl TryFrom<&[u8]> for ErrorResponse { 218 | type Error = GGError; 219 | 220 | fn try_from(value: &[u8]) -> Result { 221 | serde_json::from_slice(value).map_err(|e| { 222 | let s = String::from_utf8_lossy(value); 223 | error!("Error trying to parse error response of {}", s); 224 | Self::Error::from(e) 225 | }) 226 | } 227 | } 228 | 229 | /// Reads the response data from the gg_request_reqd call 230 | fn read_response_data(req_to_read: gg_request) -> Result, GGError> { 231 | let mut bytes: Vec = Vec::new(); 232 | 233 | unsafe { 234 | loop { 235 | let mut buffer = [0u8; BUFFER_SIZE]; 236 | let mut read: usize = 0; 237 | let raw_read = &mut read as *mut usize; 238 | 239 | let read_res = gg_request_read( 240 | req_to_read, 241 | buffer.as_mut_ptr() as *mut c_void, 242 | BUFFER_SIZE, 243 | raw_read, 244 | ); 245 | GGError::from_code(read_res)?; 246 | 247 | if read > 0 { 248 | bytes.extend_from_slice(&buffer[..read]); 249 | } else { 250 | break; 251 | } 252 | } 253 | } 254 | 255 | Ok(bytes) 256 | } 257 | 258 | #[macro_export] 259 | macro_rules! with_request { 260 | ($req:expr, $expr:block) => {{ 261 | let req_init = gg_request_init(&mut $req); 262 | GGError::from_code(req_init)?; 263 | let wrapper = move || { 264 | //this will catch the return 265 | $expr 266 | }; 267 | let output = wrapper(); 268 | let close_res = gg_request_close($req); 269 | GGError::from_code(close_res)?; 270 | output 271 | }}; 272 | } 273 | 274 | #[cfg(test)] 275 | mod test { 276 | use super::*; 277 | use std::ptr; 278 | 279 | const READ_DATA: &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Malesuada fames ac turpis egestas maecenas pharetra. Ornare massa eget egestas purus viverra accumsan in nisl nisi. Dolor morbi non arcu risus. Vehicula ipsum a arcu cursus vitae. Luctus accumsan tortor posuere ac ut consequat semper viverra. At tempor commodo ullamcorper a lacus vestibulum sed. Dui ut ornare lectus sit amet. Tristique magna sit amet purus gravida quis blandit turpis. Duis at consectetur lorem donec. Amet cursus sit amet dictum sit. Lacus viverra vitae congue eu consequat ac felis donec et. 280 | 281 | Suscipit tellus mauris a diam. Odio tempor orci dapibus ultrices in. Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Suspendisse interdum consectetur libero id faucibus nisl tincidunt eget. Non diam phasellus vestibulum lorem sed risus. Amet justo donec enim diam vulputate ut pharetra sit amet. Est ullamcorper eget nulla facilisi etiam. Ut etiam sit amet nisl purus in mollis nunc. Vel eros donec ac odio tempor. Nascetur ridiculus mus mauris vitae ultricies. 282 | 283 | Purus sit amet volutpat consequat mauris nunc. Odio ut enim blandit volutpat. Etiam tempor orci eu lobortis elementum. Praesent tristique magna sit amet purus gravida. Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Sed pulvinar proin gravida hendrerit lectus. Lacus laoreet non curabitur gravida arcu. Turpis cursus in hac habitasse platea dictumst quisque. Tempor orci eu lobortis elementum nibh. Pellentesque massa placerat duis ultricies lacus sed turpis. Ut porttitor leo a diam sollicitudin tempor id eu nisl. Justo laoreet sit amet cursus. Ultrices in iaculis nunc sed augue lacus viverra vitae. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin. Sem nulla pharetra diam sit amet nisl. Suspendisse potenti nullam ac tortor vitae purus faucibus. Odio morbi quis commodo odio aenean. Justo nec ultrices dui sapien eget mi. 284 | 285 | Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Orci sagittis eu volutpat odio facilisis mauris sit amet. Nunc non blandit massa enim. Dui ut ornare lectus sit amet est placerat in egestas. Risus sed vulputate odio ut enim blandit volutpat. Pellentesque adipiscing commodo elit at imperdiet dui accumsan. Dolor magna eget est lorem ipsum. Velit sed ullamcorper morbi tincidunt ornare massa eget. Amet commodo nulla facilisi nullam vehicula ipsum. Velit dignissim sodales ut eu sem. Sed id semper risus in hendrerit gravida rutrum. Sit amet porttitor eget dolor morbi. Blandit turpis cursus in hac. Scelerisque felis imperdiet proin fermentum leo. 286 | 287 | Facilisi nullam vehicula ipsum a arcu cursus vitae congue. Massa massa ultricies mi quis hendrerit. Sit amet facilisis magna etiam. Duis convallis convallis tellus id interdum velit laoreet id donec. Neque laoreet suspendisse interdum consectetur libero. Sed vulputate odio ut enim blandit volutpat maecenas volutpat blandit. Amet volutpat consequat mauris nunc. Erat nam at lectus urna duis convallis convallis tellus. Consectetur a erat nam at lectus urna. Iaculis at erat pellentesque adipiscing commodo elit at imperdiet. Volutpat blandit aliquam etiam erat. Semper quis lectus nulla at volutpat. Orci a scelerisque purus semper eget. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. 288 | 289 | Ultrices mi tempus imperdiet nulla. Purus in massa tempor nec feugiat nisl pretium fusce id. Praesent tristique magna sit amet purus. Facilisis volutpat est velit egestas dui. Sed egestas egestas fringilla phasellus faucibus scelerisque. Convallis a cras semper auctor. Viverra accumsan in nisl nisi. Aliquet nec ullamcorper sit amet risus. Massa sed elementum tempus egestas sed sed risus pretium. Tortor consequat id porta nibh. In tellus integer feugiat scelerisque varius morbi enim nunc. Adipiscing commodo elit at imperdiet dui accumsan. Tincidunt id aliquet risus feugiat in ante metus. Interdum varius sit amet mattis. Sit amet massa vitae tortor condimentum. Purus non enim praesent elementum. Vestibulum sed arcu non odio euismod lacinia at quis risus. Tempor id eu nisl nunc mi ipsum faucibus vitae aliquet. 290 | 291 | Vestibulum rhoncus est pellentesque elit. Feugiat nisl pretium fusce id velit ut tortor pretium. Tortor consequat id porta nibh venenatis cras sed felis eget. Velit scelerisque in dictum non consectetur a. Hendrerit gravida rutrum quisque non. Porta non pulvinar neque laoreet suspendisse interdum consectetur. Tellus rutrum tellus pellentesque eu tincidunt. Sed arcu non odio euismod lacinia at quis. Netus et malesuada fames ac turpis egestas integer eget. Vitae justo eget magna fermentum iaculis eu non. Tincidunt praesent semper feugiat nibh sed pulvinar proin. Sodales ut eu sem integer vitae justo. Enim blandit volutpat maecenas volutpat blandit aliquam. Non odio euismod lacinia at quis risus sed. Mollis nunc sed id semper. Non enim praesent elementum facilisis leo vel fringilla. Viverra vitae congue eu consequat ac felis donec. Placerat orci nulla pellentesque dignissim. Libero nunc consequat interdum varius sit amet mattis vulputate enim. Sed turpis tincidunt id aliquet. 292 | 293 | Porttitor massa id neque aliquam vestibulum morbi blandit cursus. Lacus suspendisse faucibus interdum posuere lorem. Mauris cursus mattis molestie a iaculis at erat. Blandit massa enim nec dui nunc. Arcu bibendum at varius vel pharetra vel turpis nunc eget. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Adipiscing tristique risus nec feugiat in. Egestas sed sed risus pretium quam. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget. Ac placerat vestibulum lectus mauris ultrices. Id volutpat lacus laoreet non curabitur gravida arcu ac tortor. Etiam erat velit scelerisque in dictum. At erat pellentesque adipiscing commodo. Mollis aliquam ut porttitor leo a diam sollicitudin tempor. Habitant morbi tristique senectus et netus et malesuada fames. Cras sed felis eget velit aliquet sagittis id consectetur purus. Ut sem nulla pharetra diam sit amet nisl suscipit adipiscing. Risus nec feugiat in fermentum posuere urna nec. 294 | 295 | Nibh mauris cursus mattis molestie a iaculis. Diam ut venenatis tellus in. Nisl tincidunt eget nullam non nisi est sit. Adipiscing commodo elit at imperdiet dui accumsan sit amet. Tristique senectus et netus et. Vulputate ut pharetra sit amet aliquam. Non consectetur a erat nam at lectus urna duis. Aliquet sagittis id consectetur purus ut faucibus. Eget felis eget nunc lobortis mattis aliquam faucibus. Nisl tincidunt eget nullam non nisi. Nunc eget lorem dolor sed viverra ipsum nunc. Suspendisse faucibus interdum posuere lorem. Nisl condimentum id venenatis a condimentum. Malesuada pellentesque elit eget gravida cum sociis. Porta lorem mollis aliquam ut porttitor leo a diam. Maecenas volutpat blandit aliquam etiam. 296 | 297 | Parturient montes nascetur ridiculus mus mauris vitae ultricies. Suspendisse sed nisi lacus sed viverra. Adipiscing elit pellentesque habitant morbi tristique senectus et netus et. Gravida in fermentum et sollicitudin. Sem et tortor consequat id porta nibh venenatis. Volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Amet cursus sit amet dictum sit amet justo donec enim. Nulla facilisi cras fermentum odio eu feugiat pretium nibh ipsum. Fermentum leo vel orci porta non pulvinar neque laoreet. Nunc sed id semper risus in hendrerit. Aliquet sagittis id consectetur purus ut faucibus pulvinar elementum. Tincidunt vitae semper quis lectus nulla at volutpat. Vel facilisis volutpat est velit egestas dui id ornare arcu. Vivamus arcu felis bibendum ut tristique et egestas quis. Sed vulputate odio ut enim blandit volutpat. Vel pharetra vel turpis nunc. Orci dapibus ultrices in iaculis nunc sed augue lacus. Vitae tempus quam pellentesque nec nam aliquam sem et tortor. Eget lorem dolor sed viverra ipsum. Sapien pellentesque habitant morbi tristique senectus et netus et malesuada."; 298 | 299 | #[test] 300 | fn test_read_response_data() { 301 | GG_REQUEST_READ_BUFFER.with(|buffer| buffer.replace(READ_DATA.to_owned())); 302 | let mut req: gg_request = ptr::null_mut(); 303 | let result = gg_request_init(&mut req); 304 | assert_eq!(result, gg_error_GGE_SUCCESS); 305 | 306 | let result = read_response_data(req).unwrap(); 307 | assert!(!result.is_empty()); 308 | assert_eq!(result, READ_DATA); 309 | } 310 | 311 | #[test] 312 | fn test_try_from_gg_request_status() { 313 | assert_eq!( 314 | GGRequestStatus::try_from(gg_request_status_GG_REQUEST_SUCCESS).unwrap(), 315 | GGRequestStatus::Success 316 | ); 317 | assert_eq!( 318 | GGRequestStatus::try_from(gg_request_status_GG_REQUEST_HANDLED).unwrap(), 319 | GGRequestStatus::Handled 320 | ); 321 | assert_eq!( 322 | GGRequestStatus::try_from(gg_request_status_GG_REQUEST_UNHANDLED).unwrap(), 323 | GGRequestStatus::Unhandled 324 | ); 325 | assert_eq!( 326 | GGRequestStatus::try_from(gg_request_status_GG_REQUEST_UNKNOWN).unwrap(), 327 | GGRequestStatus::Unknown 328 | ); 329 | assert_eq!( 330 | GGRequestStatus::try_from(gg_request_status_GG_REQUEST_AGAIN).unwrap(), 331 | GGRequestStatus::Again 332 | ); 333 | assert!(GGRequestStatus::try_from(9999).is_err()); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | use crate::bindings::*; 10 | use crate::error::GGError; 11 | use crate::handler::{Handler, LambdaContext}; 12 | use crate::GGResult; 13 | use crossbeam_channel::{unbounded, Receiver, Sender}; 14 | use lazy_static::lazy_static; 15 | use log::{error, info}; 16 | use std::default::Default; 17 | use std::ffi::CStr; 18 | use std::os::raw::c_void; 19 | use std::sync::Arc; 20 | use std::thread; 21 | 22 | /// The size of the buffer for reading content received via the C SDK 23 | const BUFFER_SIZE: usize = 100; 24 | 25 | /// Denotes a handler that is thread safe 26 | pub type ShareableHandler = dyn Handler + Send + Sync; 27 | 28 | lazy_static! { 29 | // This establishes a thread safe global channel that can 30 | // be acquired from the callback function we register with the C Api 31 | static ref CHANNEL: Arc = ChannelHolder::new(); 32 | } 33 | 34 | /// Type of runtime. Currently only one, Async exits 35 | pub enum RuntimeOption { 36 | /// The runtime will be started in the current thread an block preventing exit. 37 | /// This is the option that should be used for On-demand greengrass lambda functions. 38 | /// This is the default option. 39 | Sync, 40 | /// The runtime will start in a new thread. If main() exits, the runtime stops. 41 | /// This is useful for long lived lambda functions. 42 | Async, 43 | } 44 | 45 | impl RuntimeOption { 46 | /// Converts to the option required by the runtime api 47 | fn as_opt(&self) -> gg_runtime_opt { 48 | match self { 49 | Self::Sync => 0, // for some reason they don't spell out this option in the header 50 | Self::Async => gg_runtime_opt_GG_RT_OPT_ASYNC, 51 | } 52 | } 53 | } 54 | 55 | /// Configures and instantiates the green grass core runtime 56 | /// Runtime can only be started by the Initializer. You must pass the runtime into the [`Initializer::with_runtime`] method. 57 | pub struct Runtime { 58 | runtime_option: RuntimeOption, 59 | handler: Option>, 60 | } 61 | 62 | impl Default for Runtime { 63 | fn default() -> Self { 64 | Runtime { 65 | runtime_option: RuntimeOption::Sync, 66 | handler: None, 67 | } 68 | } 69 | } 70 | 71 | impl Runtime { 72 | /// Start the green grass core runtime 73 | pub(crate) fn start(self) -> GGResult<()> { 74 | unsafe { 75 | // If there is a handler defined, then register the 76 | // the c delegating handler and start a thread that 77 | // monitors the channel for messages from the c handler 78 | let c_handler = if let Some(handler) = self.handler { 79 | thread::spawn(move || loop { 80 | match ChannelHolder::recv() { 81 | Ok(context) => handler.handle(context), 82 | Err(e) => error!("{}", e), 83 | } 84 | }); 85 | 86 | delgating_handler 87 | } else { 88 | no_op_handler 89 | }; 90 | 91 | let start_res = gg_runtime_start(Some(c_handler), self.runtime_option.as_opt()); 92 | GGError::from_code(start_res)?; 93 | } 94 | Ok(()) 95 | } 96 | 97 | /// Provide a non-default runtime option 98 | pub fn with_runtime_option(self, runtime_option: RuntimeOption) -> Self { 99 | Runtime { 100 | runtime_option, 101 | ..self 102 | } 103 | } 104 | 105 | /// Provide a handler. If no handler is provided the runtime will register a no-op handler 106 | /// 107 | /// ```rust 108 | /// use aws_greengrass_core_rust::handler::{Handler, LambdaContext}; 109 | /// use aws_greengrass_core_rust::runtime::Runtime; 110 | /// 111 | /// struct MyHandler; 112 | /// 113 | /// impl Handler for MyHandler { 114 | /// fn handle(&self, ctx: LambdaContext) { 115 | /// // Do something here 116 | /// } 117 | /// } 118 | /// 119 | /// Runtime::default().with_handler(Some(Box::new(MyHandler))); 120 | /// ``` 121 | pub fn with_handler(self, handler: Option>) -> Self { 122 | Runtime { handler, ..self } 123 | } 124 | } 125 | 126 | /// c handler that performs a no op 127 | extern "C" fn no_op_handler(_: *const gg_lambda_context) { 128 | info!("No opt handler called!"); 129 | } 130 | 131 | /// c handler that utilizes ChannelHandler in order to pass 132 | /// information to the Handler implementation provided 133 | extern "C" fn delgating_handler(c_ctx: *const gg_lambda_context) { 134 | info!("delegating_handler called!"); 135 | unsafe { 136 | let result = build_context(c_ctx).and_then(ChannelHolder::send); 137 | if let Err(e) = result { 138 | error!("{}", e); 139 | } 140 | } 141 | } 142 | 143 | /// Converts the c context to our rust native context 144 | unsafe fn build_context(c_ctx: *const gg_lambda_context) -> GGResult { 145 | let message = handler_read_message()?; 146 | let function_arn = CStr::from_ptr((*c_ctx).function_arn) 147 | .to_string_lossy() 148 | .to_owned() 149 | .to_string(); 150 | let client_context = CStr::from_ptr((*c_ctx).client_context) 151 | .to_string_lossy() 152 | .to_owned() 153 | .to_string(); 154 | Ok(LambdaContext::new(function_arn, client_context, message)) 155 | } 156 | 157 | /// Wraps the C gg_lambda_handler_read call 158 | unsafe fn handler_read_message() -> GGResult> { 159 | let mut collected: Vec = Vec::new(); 160 | loop { 161 | let mut buffer = [0u8; BUFFER_SIZE]; 162 | let mut read: usize = 0; 163 | 164 | let raw_read = &mut read as *mut usize; 165 | 166 | let pub_res = 167 | gg_lambda_handler_read(buffer.as_mut_ptr() as *mut c_void, BUFFER_SIZE, raw_read); 168 | 169 | GGError::from_code(pub_res)?; 170 | 171 | if read > 0 { 172 | collected.extend_from_slice(&buffer[..read]); 173 | } else { 174 | break; 175 | } 176 | } 177 | Ok(collected) 178 | } 179 | 180 | /// Wraps a Channel. 181 | /// This is mostly needed as there is no way to instantiate a static ref with a tuple (see CHANNEL above) 182 | struct ChannelHolder { 183 | sender: Sender, 184 | receiver: Receiver, 185 | } 186 | 187 | impl ChannelHolder { 188 | pub fn new() -> Arc { 189 | let (sender, receiver) = unbounded(); 190 | let holder = ChannelHolder { sender, receiver }; 191 | Arc::new(holder) 192 | } 193 | 194 | /// Performs a send with CHANNEL and coerces the error type 195 | fn send(context: LambdaContext) -> GGResult<()> { 196 | Arc::clone(&CHANNEL) 197 | .sender 198 | .send(context) 199 | .map_err(GGError::from) 200 | } 201 | 202 | /// Performs a recv with CHANNEL and coerces the error type 203 | fn recv() -> GGResult { 204 | Arc::clone(&CHANNEL).receiver.recv().map_err(GGError::from) 205 | } 206 | } 207 | 208 | #[cfg(test)] 209 | mod test { 210 | use super::*; 211 | use crate::handler::{Handler, LambdaContext}; 212 | use crate::Initializer; 213 | use crossbeam_channel::{bounded, Sender}; 214 | use std::ffi::CString; 215 | use std::time::Duration; 216 | 217 | #[test] 218 | fn test_build_context() { 219 | unsafe { 220 | let my_message = b"My handlers message"; 221 | GG_LAMBDA_HANDLER_READ_BUFFER.with(|b| b.replace(my_message.to_owned().to_vec())); 222 | 223 | let my_function_arn = "this is a function arn"; 224 | let client_ctx = "this is my client context"; 225 | 226 | let my_function_arn_c = CString::new(my_function_arn).unwrap(); 227 | let client_ctx_c = CString::new(client_ctx).unwrap(); 228 | 229 | let lambda_context_c = Box::new(gg_lambda_context { 230 | function_arn: my_function_arn_c.as_ptr(), 231 | client_context: client_ctx_c.as_ptr(), 232 | }); 233 | 234 | let raw_ctx = Box::into_raw(lambda_context_c); 235 | let context_result = build_context(raw_ctx); 236 | let _ = Box::from_raw(raw_ctx); 237 | let context = context_result.expect("Building a context should be successful"); 238 | 239 | assert_eq!(context.function_arn, my_function_arn); 240 | assert_eq!(context.client_context, client_ctx); 241 | assert_eq!(context.message, my_message); 242 | } 243 | } 244 | 245 | #[derive(Clone)] 246 | struct TestHandler { 247 | sender: Sender, 248 | } 249 | 250 | impl TestHandler { 251 | fn new(sender: Sender) -> Self { 252 | TestHandler { sender } 253 | } 254 | } 255 | 256 | impl Handler for TestHandler { 257 | fn handle(&self, ctx: LambdaContext) { 258 | self.sender.send(ctx).expect("Could not send context"); 259 | } 260 | } 261 | 262 | #[cfg(not(feature = "mock"))] 263 | #[test] 264 | fn test_handler() { 265 | reset_test_state(); 266 | let (sender, receiver) = bounded(1); 267 | let handler = TestHandler::new(sender); 268 | let runtime = Runtime::default() 269 | .with_runtime_option(RuntimeOption::Sync) 270 | .with_handler(Some(Box::new(handler.clone()))); 271 | Initializer::default() 272 | .with_runtime(runtime) 273 | .init() 274 | .expect("Initialization failed"); 275 | let context = LambdaContext::new( 276 | "my_function_arn".to_owned(), 277 | "my_context".to_owned(), 278 | b"my bytes".to_ascii_lowercase(), 279 | ); 280 | send_to_handler(context.clone()); 281 | // a long time out in order to ensure that it will succeed when testing with coverage 282 | let ctx = receiver 283 | .recv_timeout(Duration::from_secs(120)) 284 | .expect("Context was sent within the timeout period"); 285 | assert_eq!(ctx, context); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/secret.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | //! Provides the ability to acquire secrets that have been registered with the Greengrass group 10 | //! that the lambda function has been configured to run in. 11 | 12 | use crate::bindings::*; 13 | use crate::error::GGError; 14 | use crate::request::GGRequestResponse; 15 | use crate::with_request; 16 | use crate::GGResult; 17 | use serde::Deserialize; 18 | use std::convert::From; 19 | use std::convert::TryFrom; 20 | use std::default::Default; 21 | use std::ffi::CString; 22 | use std::os::raw::c_char; 23 | use std::ptr; 24 | 25 | #[cfg(all(test, feature = "mock"))] 26 | use self::mock::*; 27 | #[cfg(all(test, feature = "mock"))] 28 | use std::rc::Rc; 29 | 30 | #[derive(Debug, Clone, Default, Deserialize, PartialEq)] 31 | #[serde(rename_all = "PascalCase")] 32 | pub struct Secret { 33 | #[serde(rename = "ARN")] 34 | pub arn: String, 35 | pub name: String, 36 | pub version_id: String, 37 | pub secret_binary: Option>, 38 | pub secret_string: Option, 39 | pub version_stages: Vec, 40 | pub created_date: i64, 41 | } 42 | 43 | impl Secret { 44 | /// For testing purposes. 45 | /// Can be called with default() to provide a string value 46 | #[cfg(test)] 47 | pub fn with_secret_string(self, secret_string: Option) -> Self { 48 | Secret { 49 | secret_string, 50 | ..self 51 | } 52 | } 53 | } 54 | 55 | /// Handles requests to the SecretManager secrets 56 | /// that have been exposed to the green grass lambda 57 | /// 58 | /// ```rust 59 | /// use aws_greengrass_core_rust::secret::SecretClient; 60 | /// 61 | /// let secret_result = SecretClient::default().for_secret_id("mysecret") 62 | /// .with_secret_version(Some("version here".to_owned())) 63 | /// .request(); 64 | /// ``` 65 | #[derive(Clone)] 66 | pub struct SecretClient { 67 | #[cfg(all(test, feature = "mock"))] 68 | pub mocks: Rc, 69 | } 70 | 71 | impl SecretClient { 72 | /// Creates a new SecretRequestBuilder using the specified secret_id 73 | /// 74 | /// * `secret_id` - The full arn or simple name of the secret 75 | pub fn for_secret_id(&self, secret_id: &str) -> SecretRequestBuilder { 76 | SecretRequestBuilder { 77 | secret_id: secret_id.to_owned(), 78 | secret_version: None, 79 | secret_version_stage: None, 80 | #[cfg(all(test, feature = "mock"))] 81 | mocks: Rc::clone(&self.mocks), 82 | } 83 | } 84 | 85 | /// Use the specified mock holder 86 | #[cfg(all(test, feature = "mock"))] 87 | pub fn with_mocks(self, mocks: Rc) -> Self { 88 | SecretClient { mocks, ..self } 89 | } 90 | } 91 | 92 | impl Default for SecretClient { 93 | fn default() -> Self { 94 | SecretClient { 95 | #[cfg(all(test, feature = "mock"))] 96 | mocks: Rc::new(MockHolder::default()), 97 | } 98 | } 99 | } 100 | 101 | /// Used to construct a request to send to acquire a secret from Greengrass 102 | #[derive(Clone)] 103 | pub struct SecretRequestBuilder { 104 | pub secret_id: String, 105 | pub secret_version: Option, 106 | pub secret_version_stage: Option, 107 | #[cfg(all(test, feature = "mock"))] 108 | pub mocks: Rc, 109 | } 110 | 111 | impl SecretRequestBuilder { 112 | /// Optional Secret version 113 | pub fn with_secret_version(self, secret_version: Option) -> Self { 114 | SecretRequestBuilder { 115 | secret_version, 116 | ..self 117 | } 118 | } 119 | 120 | /// Optional secret stage 121 | pub fn with_secret_version_stage(self, secret_version_stage: Option) -> Self { 122 | SecretRequestBuilder { 123 | secret_version_stage, 124 | ..self 125 | } 126 | } 127 | 128 | /// Executes the request and returns the secret 129 | #[cfg(not(all(test, feature = "mock")))] 130 | pub fn request(&self) -> GGResult> { 131 | if let Some(response) = read_secret(self)? { 132 | Ok(Some(self.parse_response(&response)?)) 133 | } else { 134 | Ok(None) 135 | } 136 | } 137 | 138 | fn parse_response(&self, response: &[u8]) -> GGResult { 139 | serde_json::from_slice::(response).map_err(GGError::from) 140 | } 141 | 142 | // ----------------------------------- 143 | // Mock methods 144 | // ----------------------------------- 145 | 146 | #[cfg(all(test, feature = "mock"))] 147 | pub fn request(&self) -> GGResult> { 148 | log::warn!("Mock request is being executed!!! This should not happen in prod!!!!"); 149 | self.mocks.request_inputs.borrow_mut().push(self.clone()); 150 | 151 | if let Some(output) = Rc::clone(&self.mocks).request_outputs.borrow_mut().pop() { 152 | output 153 | } else { 154 | Ok(Some(Secret::default())) 155 | } 156 | } 157 | } 158 | /// Fetch the specified secrete from the green grass secret store 159 | fn read_secret(builder: &SecretRequestBuilder) -> GGResult>> { 160 | unsafe { 161 | let secret_name_c = CString::new(builder.secret_id.as_str()).map_err(GGError::from)?; 162 | let maybe_secret_version_c = if let Some(secret_version) = &builder.secret_version { 163 | Some(CString::new(secret_version.as_str()).map_err(GGError::from)?) 164 | } else { 165 | None 166 | }; 167 | 168 | let maybe_secret_stage_c = if let Some(stage) = &builder.secret_version_stage { 169 | Some(CString::new(stage.as_str()).map_err(GGError::from)?) 170 | } else { 171 | None 172 | }; 173 | 174 | let mut req: gg_request = ptr::null_mut(); 175 | with_request!(req, { 176 | let mut res = gg_request_result { 177 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 178 | }; 179 | 180 | let fetch_res = gg_get_secret_value( 181 | req, 182 | secret_name_c.as_ptr(), 183 | maybe_secret_version_c 184 | .map(|c| c.as_ptr()) 185 | .unwrap_or(ptr::null() as *const c_char), 186 | maybe_secret_stage_c 187 | .map(|c| c.as_ptr()) 188 | .unwrap_or(ptr::null() as *const c_char), 189 | &mut res, 190 | ); 191 | GGError::from_code(fetch_res)?; 192 | let response = GGRequestResponse::try_from(&res)?; 193 | response.read(req) 194 | }) 195 | } 196 | } 197 | 198 | #[cfg(all(test, feature = "mock"))] 199 | mod mock { 200 | use super::*; 201 | use crate::secret::SecretRequestBuilder; 202 | use std::cell::RefCell; 203 | 204 | pub struct MockHolder { 205 | pub request_inputs: RefCell>, 206 | pub request_outputs: RefCell>>>, 207 | } 208 | 209 | impl MockHolder { 210 | pub fn with_request_outputs(self, request_outputs: Vec>>) -> Self { 211 | MockHolder { 212 | request_outputs: RefCell::new(request_outputs), 213 | ..self 214 | } 215 | } 216 | } 217 | 218 | impl Default for MockHolder { 219 | fn default() -> Self { 220 | MockHolder { 221 | request_inputs: RefCell::new(vec![]), 222 | // NOTE: We can't copy the outputs since result isn't cloneable, so just empty it 223 | request_outputs: RefCell::new(vec![]), 224 | } 225 | } 226 | } 227 | 228 | // Note: This is to get past compile issues.. Mock testing for threads 229 | // could result in undefined behavior 230 | unsafe impl Send for MockHolder {} 231 | 232 | unsafe impl Sync for MockHolder {} 233 | 234 | #[cfg(test)] 235 | mod test { 236 | use super::*; 237 | 238 | #[test] 239 | fn test_mocks() { 240 | let secret_id = "my secret 111"; 241 | let secret = Secret::default().with_secret_string(Some(secret_id.to_owned())); 242 | let mocks = MockHolder::default().with_request_outputs(vec![Ok(Some(secret))]); 243 | 244 | let client = SecretClient::default().with_mocks(Rc::new(mocks)); 245 | 246 | let result = client.for_secret_id(secret_id).request().unwrap().unwrap(); 247 | assert_eq!(result.secret_string.unwrap(), secret_id); 248 | } 249 | 250 | #[test] 251 | fn test_mocks_err() { 252 | let secret_id = "my secret 112"; 253 | let err_str = "Foo!"; 254 | let mocks = MockHolder::default() 255 | .with_request_outputs(vec![Err(GGError::Unknown(err_str.to_owned()))]); 256 | 257 | let client = SecretClient::default().with_mocks(Rc::new(mocks)); 258 | 259 | if let GGError::Unknown(msg) = client.for_secret_id(secret_id).request().unwrap_err() { 260 | assert_eq!(msg, err_str); 261 | } else { 262 | panic!("wrong error type"); 263 | } 264 | } 265 | } 266 | } 267 | 268 | #[cfg(test)] 269 | mod tests { 270 | use super::*; 271 | 272 | const ARN: &'static str = "arn:aws:secretsmanager:us-west-2:701603852992:secret:greengrass-vendor-adapter-tls-secret-EZB0nM"; 273 | const VERSION_ID: &'static str = "55acd8c0-ff58-4197-9b69-8772ea761ed4"; 274 | const SECRET_STRING: &'static str = "foo"; 275 | const NAME: &'static str = "greengrass-vendor-adapter-tls-secret"; 276 | const CREATION_DATE: i64 = 1580414897159; 277 | const VERSION_STAGE: &'static str = "AWSCURRENT"; 278 | 279 | fn version_stages() -> Vec { 280 | vec![VERSION_STAGE.to_owned()] 281 | } 282 | 283 | fn test_response() -> String { 284 | format!("{{\"ARN\":\"{}\",\"Name\":\"{}\",\"VersionId\":\"{}\",\"SecretBinary\":null,\"SecretString\":\"{}\",\"VersionStages\":{:?},\"CreatedDate\":{} }}", ARN, NAME, VERSION_ID, SECRET_STRING, version_stages(), CREATION_DATE) 285 | } 286 | 287 | #[test] 288 | fn test_parse_response() { 289 | let response = test_response(); 290 | println!("{}", response); 291 | let secret: Secret = serde_json::from_str(&response).unwrap(); 292 | assert_eq!(ARN, secret.arn); 293 | assert_eq!(VERSION_ID, secret.version_id); 294 | assert_eq!(SECRET_STRING, secret.secret_string.unwrap()); 295 | assert_eq!(NAME, secret.name); 296 | assert_eq!(CREATION_DATE, secret.created_date); 297 | assert_eq!(version_stages(), secret.version_stages); 298 | } 299 | 300 | #[cfg(not(feature = "mock"))] 301 | #[test] 302 | fn test_for_secret_id_request() { 303 | reset_test_state(); 304 | let secret_id = "my_secret_id"; 305 | GG_REQUEST_READ_BUFFER.with(|rc| rc.replace(test_response().into_bytes())); 306 | let secret = SecretClient::default() 307 | .for_secret_id(secret_id) 308 | .request() 309 | .unwrap() 310 | .unwrap(); 311 | let assert_secret_string = test_response(); 312 | assert_eq!( 313 | secret, 314 | serde_json::from_str::(assert_secret_string.as_str()).unwrap() 315 | ); 316 | GG_GET_SECRET_VALUE_ARGS.with(|rc| { 317 | let borrowed = rc.borrow(); 318 | assert_eq!(borrowed.secret_id, secret_id); 319 | }); 320 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 321 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 322 | } 323 | 324 | #[cfg(not(feature = "mock"))] 325 | #[test] 326 | fn test_for_secret_gg_error() { 327 | let error = gg_error_GGE_INVALID_STATE; 328 | GG_GET_SECRET_VALUE_RETURN.with(|rc| rc.replace(error)); 329 | let secret_id = "failed_secret_id"; 330 | let secret = SecretClient::default().for_secret_id(secret_id).request(); 331 | assert!(secret.is_err()); 332 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 333 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 334 | if let Err(GGError::InvalidState) = secret { 335 | // don't fail 336 | } else { 337 | panic!("There should have been an Invalid Err"); 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/shadow.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-present, Nike, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the Apache-2.0 license found in 6 | * the LICENSE file in the root of this source tree. 7 | */ 8 | 9 | use serde_json; 10 | use std::convert::TryFrom; 11 | use std::ffi::CString; 12 | use std::ptr; 13 | 14 | use crate::bindings::*; 15 | use crate::error::GGError; 16 | use crate::request::GGRequestResponse; 17 | use crate::with_request; 18 | use crate::GGResult; 19 | use serde::de::DeserializeOwned; 20 | use serde::Serialize; 21 | use std::default::Default; 22 | 23 | #[cfg(all(test, feature = "mock"))] 24 | use self::mock::*; 25 | 26 | /// Provides the ability to interact with a Thing's (Device) Shadow document 27 | /// 28 | /// Information on shadow documents can be found at: https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-document.html#device-shadow-example 29 | #[derive(Clone)] 30 | pub struct ShadowClient { 31 | /// When the mock feature is turned on this field will contain captured input 32 | /// and values to be returned 33 | #[cfg(all(test, feature = "mock"))] 34 | pub mocks: MockHolder, 35 | } 36 | 37 | impl ShadowClient { 38 | /// Get thing shadow for thing name. 39 | /// 40 | /// # Arguments 41 | /// 42 | /// * `thing_name` - The name of the device for the thing shadow to get 43 | /// 44 | /// # Example 45 | /// 46 | /// ```rust 47 | /// use serde_json::Value; 48 | /// use aws_greengrass_core_rust::shadow::ShadowClient; 49 | /// 50 | /// if let Ok(maybe_json) = ShadowClient::default().get_thing_shadow::("my_thing") { 51 | /// println!("Retrieved: {:?}", maybe_json); 52 | /// } 53 | /// ``` 54 | #[cfg(not(all(test, feature = "mock")))] 55 | pub fn get_thing_shadow(&self, thing_name: &str) -> GGResult> { 56 | if let Some(bytes) = read_thing_shadow(thing_name)? { 57 | let json: T = serde_json::from_slice(&bytes).map_err(GGError::from)?; 58 | Ok(Some(json)) 59 | } else { 60 | Ok(None) 61 | } 62 | } 63 | 64 | /// Updates a shadow thing with the specified document. 65 | /// 66 | /// # Arguments 67 | /// 68 | /// * `thing_name` - The name of the device to update the shadow document 69 | /// * `doc` - Json serializable content to update 70 | /// 71 | /// # Examples 72 | /// ```rust 73 | /// use serde::Serialize; 74 | /// use aws_greengrass_core_rust::shadow::ShadowClient; 75 | /// 76 | /// #[derive(Serialize)] 77 | /// struct MyStruct; 78 | /// 79 | /// let result = ShadowClient::default().update_thing_shadow("foo", &MyStruct); 80 | /// ``` 81 | #[cfg(not(all(test, feature = "mock")))] 82 | pub fn update_thing_shadow(&self, thing_name: &str, doc: &T) -> GGResult<()> { 83 | let json_string = serde_json::to_string(doc).map_err(GGError::from)?; 84 | unsafe { 85 | let thing_name_c = CString::new(thing_name).map_err(GGError::from)?; 86 | let json_string_c = CString::new(json_string).map_err(GGError::from)?; 87 | let mut req: gg_request = ptr::null_mut(); 88 | with_request!(req, { 89 | let mut res = gg_request_result { 90 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 91 | }; 92 | let update_res = gg_update_thing_shadow( 93 | req, 94 | thing_name_c.as_ptr(), 95 | json_string_c.as_ptr(), 96 | &mut res, 97 | ); 98 | GGError::from_code(update_res)?; 99 | GGRequestResponse::try_from(&res)?.to_error_result(req) 100 | }) 101 | } 102 | } 103 | 104 | /// Deletes thing shadow for thing name. 105 | /// 106 | /// # Arguments 107 | /// 108 | /// * `thing_name` - The name of the device for the thing shadow to get 109 | /// 110 | /// # Example 111 | /// 112 | /// ```rust 113 | /// use serde_json::Value; 114 | /// use aws_greengrass_core_rust::shadow::ShadowClient; 115 | /// 116 | /// let res = ShadowClient::default().delete_thing_shadow("my_thing"); 117 | /// ``` 118 | #[cfg(not(all(test, feature = "mock")))] 119 | pub fn delete_thing_shadow(&self, thing_name: &str) -> GGResult<()> { 120 | unsafe { 121 | let thing_name_c = CString::new(thing_name).map_err(GGError::from)?; 122 | let mut req: gg_request = ptr::null_mut(); 123 | with_request!(req, { 124 | let mut res_c = gg_request_result { 125 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 126 | }; 127 | let delete_res = gg_delete_thing_shadow(req, thing_name_c.as_ptr(), &mut res_c); 128 | GGError::from_code(delete_res)?; 129 | GGRequestResponse::try_from(&res_c)?.to_error_result(req) 130 | }) 131 | } 132 | } 133 | 134 | // ----------------------------------- 135 | // Mock methods 136 | // ----------------------------------- 137 | 138 | #[cfg(all(test, feature = "mock"))] 139 | pub fn get_thing_shadow<'a, T: DeserializeOwned>( 140 | &self, 141 | thing_name: &str, 142 | ) -> GGResult> { 143 | self.mocks 144 | .get_shadow_thing_inputs 145 | .borrow_mut() 146 | .push(GetShadowThingInput(thing_name.to_owned())); 147 | if let Some(output) = self.mocks.get_shadow_thing_outputs.borrow_mut().pop() { 148 | output.map(|o| serde_json::from_slice::(o.as_slice()).ok()) 149 | } else { 150 | Ok(serde_json::from_str(self::test::DEFAULT_SHADOW_DOC).ok()) 151 | } 152 | } 153 | 154 | #[cfg(all(test, feature = "mock"))] 155 | pub fn update_thing_shadow(&self, thing_name: &str, doc: &T) -> GGResult<()> { 156 | let bytes = serde_json::to_vec(doc).map_err(GGError::from)?; 157 | self.mocks 158 | .update_thing_shadow_inputs 159 | .borrow_mut() 160 | .push(UpdateThingShadowInput(thing_name.to_owned(), bytes)); 161 | if let Some(output) = self.mocks.update_thing_shadow_outputs.borrow_mut().pop() { 162 | output 163 | } else { 164 | Ok(()) 165 | } 166 | } 167 | 168 | #[cfg(all(test, feature = "mock"))] 169 | pub fn delete_thing_shadow(&self, thing_name: &str) -> GGResult<()> { 170 | self.mocks 171 | .delete_thing_shadow_inputs 172 | .borrow_mut() 173 | .push(DeleteThingShadowInput(thing_name.to_owned())); 174 | if let Some(output) = self.mocks.delete_thing_shadow_outputs.borrow_mut().pop() { 175 | output 176 | } else { 177 | Ok(()) 178 | } 179 | } 180 | } 181 | 182 | impl Default for ShadowClient { 183 | fn default() -> Self { 184 | ShadowClient { 185 | #[cfg(all(test, feature = "mock"))] 186 | mocks: MockHolder::default(), 187 | } 188 | } 189 | } 190 | 191 | fn read_thing_shadow(thing_name: &str) -> GGResult>> { 192 | unsafe { 193 | let thing_name_c = CString::new(thing_name).map_err(GGError::from)?; 194 | let mut req: gg_request = ptr::null_mut(); 195 | with_request!(req, { 196 | let mut res = gg_request_result { 197 | request_status: gg_request_status_GG_REQUEST_SUCCESS, 198 | }; 199 | let fetch_res = gg_get_thing_shadow(req, thing_name_c.as_ptr(), &mut res); 200 | GGError::from_code(fetch_res)?; 201 | GGRequestResponse::try_from(&res)?.read(req) 202 | }) 203 | } 204 | } 205 | 206 | #[cfg(all(test, feature = "mock"))] 207 | pub mod mock { 208 | use crate::GGResult; 209 | use serde::Serialize; 210 | use std::cell::RefCell; 211 | 212 | #[derive(Debug, Clone)] 213 | pub struct GetShadowThingInput(pub String); 214 | /// second parameter is serde serialized parameter 215 | #[derive(Debug, Clone)] 216 | pub struct UpdateThingShadowInput(pub String, pub Vec); 217 | #[derive(Debug, Clone)] 218 | pub struct DeleteThingShadowInput(pub String); 219 | 220 | /// Used to hold inputs and override default outputs for mocks 221 | pub struct MockHolder { 222 | pub get_shadow_thing_inputs: RefCell>, 223 | /// By providing the byte vec we can get around the generic type signature 224 | /// issue. This should be Deserializable to the type parameter passed to the method 225 | pub get_shadow_thing_outputs: RefCell>>>, 226 | /// Serializable representation to get past generics issue 227 | pub update_thing_shadow_inputs: RefCell>, 228 | pub update_thing_shadow_outputs: RefCell>>, 229 | pub delete_thing_shadow_inputs: RefCell>, 230 | pub delete_thing_shadow_outputs: RefCell>>, 231 | } 232 | 233 | impl Clone for MockHolder { 234 | fn clone(&self) -> Self { 235 | MockHolder { 236 | get_shadow_thing_inputs: self.get_shadow_thing_inputs.clone(), 237 | update_thing_shadow_inputs: self.update_thing_shadow_inputs.clone(), 238 | delete_thing_shadow_inputs: self.delete_thing_shadow_inputs.clone(), 239 | // NOTE: Cannot clone outputs. Keep this in mind in tests 240 | get_shadow_thing_outputs: RefCell::new(vec![]), 241 | update_thing_shadow_outputs: RefCell::new(vec![]), 242 | delete_thing_shadow_outputs: RefCell::new(vec![]), 243 | } 244 | } 245 | } 246 | 247 | impl Default for MockHolder { 248 | fn default() -> Self { 249 | MockHolder { 250 | get_shadow_thing_inputs: RefCell::new(vec![]), 251 | update_thing_shadow_inputs: RefCell::new(vec![]), 252 | delete_thing_shadow_inputs: RefCell::new(vec![]), 253 | get_shadow_thing_outputs: RefCell::new(vec![]), 254 | update_thing_shadow_outputs: RefCell::new(vec![]), 255 | delete_thing_shadow_outputs: RefCell::new(vec![]), 256 | } 257 | } 258 | } 259 | 260 | // Note: This is to get past compile issues.. Mock testing for threads 261 | // could result in undefined behavior 262 | unsafe impl Send for MockHolder {} 263 | unsafe impl Sync for MockHolder {} 264 | } 265 | 266 | #[cfg(test)] 267 | pub mod test { 268 | use super::*; 269 | use serde_json::Value; 270 | 271 | pub const DEFAULT_SHADOW_DOC: &'static str = r#"{ 272 | "state" : { 273 | "desired" : { 274 | "color" : "RED", 275 | "sequence" : [ "RED", "GREEN", "BLUE" ] 276 | }, 277 | "reported" : { 278 | "color" : "GREEN" 279 | } 280 | }, 281 | "metadata" : { 282 | "desired" : { 283 | "color" : { 284 | "timestamp" : 12345 285 | }, 286 | "sequence" : { 287 | "timestamp" : 12345 288 | } 289 | }, 290 | "reported" : { 291 | "color" : { 292 | "timestamp" : 12345 293 | } 294 | } 295 | }, 296 | "version" : 10, 297 | "clientToken" : "UniqueClientToken", 298 | "timestamp": 123456789 299 | }"#; 300 | 301 | #[cfg(not(feature = "mock"))] 302 | #[test] 303 | fn test_get_shadow_thing() { 304 | reset_test_state(); 305 | GG_REQUEST_READ_BUFFER.with(|rc| rc.replace(DEFAULT_SHADOW_DOC.as_bytes().to_vec())); 306 | let thing_name = "my_thing_get"; 307 | let shadow = ShadowClient::default() 308 | .get_thing_shadow::(thing_name) 309 | .unwrap() 310 | .unwrap(); 311 | GG_SHADOW_THING_ARG.with(|rc| assert_eq!(*rc.borrow(), thing_name)); 312 | assert_eq!( 313 | shadow, 314 | serde_json::from_str::(DEFAULT_SHADOW_DOC).unwrap() 315 | ); 316 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 317 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 318 | } 319 | 320 | #[cfg(not(feature = "mock"))] 321 | #[test] 322 | fn test_delete_shadow_thing() { 323 | reset_test_state(); 324 | let thing_name = "my_thing_get_delete"; 325 | ShadowClient::default() 326 | .delete_thing_shadow(thing_name) 327 | .unwrap(); 328 | GG_SHADOW_THING_ARG.with(|rc| assert_eq!(*rc.borrow(), thing_name)); 329 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 330 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 331 | } 332 | 333 | #[cfg(not(feature = "mock"))] 334 | #[test] 335 | fn test_update_shadow_thing() { 336 | reset_test_state(); 337 | let thing_name = "my_thing_update"; 338 | let doc = serde_json::from_str::(DEFAULT_SHADOW_DOC).unwrap(); 339 | ShadowClient::default() 340 | .update_thing_shadow(thing_name, &doc) 341 | .unwrap(); 342 | GG_SHADOW_THING_ARG.with(|rc| assert_eq!(*rc.borrow(), thing_name)); 343 | GG_UPDATE_PAYLOAD.with(|rc| { 344 | assert_eq!(*rc.borrow(), serde_json::to_string(&doc).unwrap()); 345 | }); 346 | GG_CLOSE_REQUEST_COUNT.with(|rc| assert_eq!(*rc.borrow(), 1)); 347 | GG_REQUEST.with(|rc| assert!(!rc.borrow().is_default())); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /stubs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(greengrasssdkstub) 4 | 5 | ############################################################ 6 | # Create a library 7 | ############################################################ 8 | 9 | #Generate the shared library from the library sources 10 | add_library(greengrasssdk SHARED 11 | src/greengrasssdk.c 12 | ) 13 | add_library(greengrasssdk::library ALIAS greengrasssdk) 14 | 15 | set_target_properties(greengrasssdk PROPERTIES OUTPUT_NAME "aws-greengrass-core-sdk-c") 16 | target_include_directories(greengrasssdk 17 | PUBLIC 18 | ${PROJECT_SOURCE_DIR}/include 19 | ) 20 | 21 | install(TARGETS greengrasssdk DESTINATION lib) 22 | install(FILES include/shared/greengrasssdk.h DESTINATION include) 23 | -------------------------------------------------------------------------------- /stubs/README.md: -------------------------------------------------------------------------------- 1 | #Green Grass API stubs 2 | 3 | This project is useful when building green grass locally as the SDK doesn't build on Mac OS X. 4 | 5 | ## Prerequsites 6 | * Cmake is installed ```brew install cmake``` 7 | 8 | ## Building and installing 9 | 1. Clone this repo 10 | 2. cd greengrasssdkstub 11 | 3. mkdir build 12 | 4. cd build 13 | 5. cmake .. 14 | 6. make 15 | 7. make install -------------------------------------------------------------------------------- /stubs/include/shared/greengrasssdk.h: -------------------------------------------------------------------------------- 1 | /** Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ 2 | 3 | /** 4 | * @file greengrasssdk.h 5 | * @brief Definition of SDK interfaces. 6 | */ 7 | #ifndef _GREENGRASS_SDK_H_ 8 | #define _GREENGRASS_SDK_H_ 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | /*************************************** 19 | ** Greengrass Types ** 20 | ***************************************/ 21 | 22 | /** 23 | * @brief Greengrass SDK error enum 24 | * 25 | * Enumeration of return values from the gg_* functions within the SDK. 26 | */ 27 | typedef enum gg_error { 28 | /** Returned when success */ 29 | GGE_SUCCESS = 0, 30 | /** Returned when process is out of memory */ 31 | GGE_OUT_OF_MEMORY, 32 | /** Returned when input parameter is invalid */ 33 | GGE_INVALID_PARAMETER, 34 | /** Returned when SDK is in an invalid state */ 35 | GGE_INVALID_STATE, 36 | /** Returned when SDK encounters internal failure */ 37 | GGE_INTERNAL_FAILURE, 38 | /** Returned when process gets signal to terminate */ 39 | GGE_TERMINATE, 40 | 41 | GGE_RESERVED_MAX, 42 | GGE_RESERVED_PAD = 0x7FFFFFFF 43 | } gg_error; 44 | 45 | typedef struct _gg_request *gg_request; 46 | 47 | /** 48 | * @brief Greengrass SDK request status enum 49 | * 50 | * Enumeration of status populated when **gg_invoke()**, **gg_publish()** or 51 | * **gg_xxx_thing_shadow()** function is called. 52 | */ 53 | typedef enum gg_request_status { 54 | /** function call returns expected payload type */ 55 | GG_REQUEST_SUCCESS = 0, 56 | /** function call is successfull, however lambda responss with an error */ 57 | GG_REQUEST_HANDLED, 58 | /** function call is unsuccessfull, lambda exits abnormally */ 59 | GG_REQUEST_UNHANDLED, 60 | /** System encounters unknown error. Check logs for more details */ 61 | GG_REQUEST_UNKNOWN, 62 | /** function call is throttled, try again */ 63 | GG_REQUEST_AGAIN, 64 | 65 | GG_REQUEST_RESERVED_MAX, 66 | GG_REQUEST_RESERVED_PAD = 0x7FFFFFFF 67 | } gg_request_status; 68 | 69 | /** 70 | * @brief Describes result metadata after request is made 71 | * @param request_status the request status 72 | */ 73 | typedef struct gg_request_result { 74 | /** Status enum after request is made */ 75 | gg_request_status request_status; 76 | } gg_request_result; 77 | 78 | /** 79 | * @brief Describes context when lambda handler is called 80 | * @param function_arn Null-terminated string full lambda ARN 81 | * @param client_context Null-terminated string of client context 82 | */ 83 | typedef struct gg_lambda_context { 84 | const char *function_arn; 85 | const char *client_context; 86 | } gg_lambda_context; 87 | 88 | /** 89 | * @brief Describes invocation type for lambda function 90 | */ 91 | typedef enum gg_invoke_type { 92 | /** Invoke the function asynchronously */ 93 | GG_INVOKE_EVENT, 94 | /** Invoke the function synchronously (default) */ 95 | GG_INVOKE_REQUEST_RESPONSE, 96 | 97 | GG_INVOKE_RESERVED_MAX, 98 | GG_INVOKE_RESERVED_PAD = 0x7FFFFFFF 99 | } gg_invoke_type; 100 | 101 | /** 102 | * @brief Flags set for the gg_runtime_start)(.., opt) 103 | */ 104 | typedef enum gg_runtime_opt { 105 | /** Start the runtime in a new thread. Runtime will exit if main thread exits */ 106 | GG_RT_OPT_ASYNC = 0x1, 107 | GG_RT_OPT_RESERVED_PAD = 0x7FFFFFFF 108 | } gg_runtime_opt; 109 | 110 | /** 111 | * @brief Describes the options to invoke a target lambda 112 | * 113 | * @param function_arn Null-terminated string full lambda ARN to be invoked 114 | * @param customer_context base64-encoded null-terminated json string 115 | * @param qualifier Null-terminated string version of the function 116 | * @param type Specifiy whether a response is needed 117 | * @param payload Buffer to be sent to the invoked lambda 118 | * @param payload_size Size of payload buffer 119 | */ 120 | typedef struct gg_invoke_options { 121 | const char *function_arn; 122 | const char *customer_context; 123 | const char *qualifier; 124 | gg_invoke_type type; 125 | const void *payload; 126 | size_t payload_size; 127 | } gg_invoke_options; 128 | 129 | /** 130 | * @brief Describes the policy options to take when Greengrass's queue is full 131 | */ 132 | typedef enum gg_queue_full_policy_options { 133 | /** GGC will deliver messages to as many targets as possible **/ 134 | GG_QUEUE_FULL_POLICY_BEST_EFFORT, 135 | /** GGC will either deliver messages to all targets and return request 136 | * status GG_REQUEST_SUCCESS or deliver to no targets and return a 137 | * request status GG_REQUEST_AGAIN **/ 138 | GG_QUEUE_FULL_POLICY_ALL_OR_ERROR, 139 | 140 | GG_QUEUE_FULL_POLICY_RESERVED_MAX, 141 | GG_QUEUE_FULL_POLICY_RESERVED_PAD = 0x7FFFFFFF 142 | } gg_queue_full_policy_options; 143 | 144 | typedef struct _gg_publish_options *gg_publish_options; 145 | 146 | /** 147 | * @brief Describes log levels could used in **gg_log()** 148 | */ 149 | typedef enum gg_log_level { 150 | GG_LOG_RESERVED_NOTSET, 151 | 152 | /** Debug */ 153 | GG_LOG_DEBUG, 154 | /** Info */ 155 | GG_LOG_INFO, 156 | /** Warn */ 157 | GG_LOG_WARN, 158 | /** Error */ 159 | GG_LOG_ERROR, 160 | /** Fatal. System will exist */ 161 | GG_LOG_FATAL, 162 | 163 | GG_LOG_RESERVED_MAX, 164 | GG_LOG_RESERVED_PAD = 0x7FFFFFFF 165 | } gg_log_level; 166 | 167 | /*************************************** 168 | ** Global Methods ** 169 | ***************************************/ 170 | 171 | 172 | /** 173 | * @brief Initialize Greengrass internal global variables 174 | * @param opt Reserved for future use. Must be set to 0. 175 | * @return Greengrass error code 176 | * @note THIS IS NOT THREAD SAFE and must be called when there is only a single 177 | * main thread executing. 178 | * User must call this function before creating any threads. 179 | * User must call this function before calling any other Greengrass 180 | * function in this SDK. 181 | */ 182 | gg_error gg_global_init(uint32_t opt); 183 | 184 | /*************************************** 185 | ** Logging Methods ** 186 | ***************************************/ 187 | 188 | /** 189 | * @brief Log message to Greengrass Core using similar syntax to printf 190 | * @param level Level of message that can be filtered based on settings 191 | * @param format Similar to printf 192 | * @param ... Similar to printf 193 | * @return Greengrass error code 194 | */ 195 | gg_error gg_log(gg_log_level level, const char *format, ...); 196 | 197 | /*************************************** 198 | ** gg_request Methods ** 199 | ***************************************/ 200 | 201 | /** 202 | * @brief Initialize the context for managing the request 203 | * @param ggreq Pointer to context to be initialized 204 | * @return Greengrass error code 205 | * @note Need to call gg_request_close on ggreq when done using it 206 | */ 207 | gg_error gg_request_init(gg_request *ggreq); 208 | 209 | /** 210 | * @brief Close a request context that was created by gg_request_init 211 | * @param ggreq Context to be closed 212 | * @return Greengrass error code 213 | */ 214 | gg_error gg_request_close(gg_request ggreq); 215 | 216 | /** 217 | * @brief Read the data from a request. This method should be called 218 | * till amount_read is zero. 219 | * 220 | * @param ggreq Provides context about the request 221 | * @param buffer Destination for read data 222 | * @param buffer_size Size of buffer 223 | * @param amount_read Destination for amount of data read into buffer 224 | * @return Greengrass error code 225 | */ 226 | gg_error gg_request_read(gg_request ggreq, void *buffer, size_t buffer_size, 227 | size_t *amount_read); 228 | 229 | /*************************************** 230 | ** Runtime Methods ** 231 | ***************************************/ 232 | 233 | /** 234 | * @brief Handler signature that will be called whenever a subscribed message 235 | * is received 236 | * @param cxt Details about the lambda invocation 237 | */ 238 | typedef void (*gg_lambda_handler)(const gg_lambda_context *cxt); 239 | 240 | /** 241 | * @brief Registers the lambda handler and start Greengrass lambda runtime 242 | * 243 | * @param handler Customer lambda code to be run when subscription is triggered 244 | * @param opt Mask flags of gg_runtime_opt options, 0 for default 245 | * @note Must be called. This uses and will overwrite the SIGTERM handler 246 | */ 247 | gg_error gg_runtime_start(gg_lambda_handler handler, uint32_t opt); 248 | 249 | /** 250 | * @brief Read the data from the invoker of the lambda. This method should be called 251 | * till amount_read is zero. 252 | * 253 | * @param buffer Destination for read data 254 | * @param buffer_size Size of buffer 255 | * @param amount_read Destination for amount of data read into buffer 256 | * @return Greengrass error code 257 | * @note This should only be used in the lambda handler 258 | */ 259 | gg_error gg_lambda_handler_read(void *buffer, size_t buffer_size, 260 | size_t *amount_read); 261 | 262 | /** 263 | * @brief Write response to the invoker of the lambda 264 | * @param response Response data to be written 265 | * @param response_size Amount of data stored in response 266 | * @return Greengrass error code 267 | * @note This should only be used in the lambda handler 268 | */ 269 | gg_error gg_lambda_handler_write_response(const void *response, 270 | size_t response_size); 271 | 272 | /** 273 | * @brief Write error message to the invoker of the lambda 274 | * @param error_message Null-terminated string error message to be written 275 | * @return Greengrass error code 276 | * @note This should only be used in the lambda handler 277 | * @note The caller's invoke will return result GG_REQUEST_HANDLED 278 | * in the gg_request_result struct instead of GG_REQUEST_SUCCESS. 279 | */ 280 | gg_error gg_lambda_handler_write_error(const char *error_message); 281 | 282 | /*************************************** 283 | ** AWS Secrets Manager Methods ** 284 | ***************************************/ 285 | 286 | /** 287 | * @brief Get secret value for the given secret 288 | * @param ggreq Provides context about the request 289 | * @param secret_id Null-terminated string id which secret to get 290 | * @param version_id Null-terminated string version id which version to get 291 | * @param version_stage Optional null-terminated string version stage which stage to get 292 | * @param result Describes the result of the request 293 | * @return Greengrass error code 294 | */ 295 | gg_error gg_get_secret_value(gg_request ggreq, const char *secret_id, 296 | const char *version_id, const char *version_stage, 297 | gg_request_result *result); 298 | 299 | /*************************************** 300 | ** Lambda Methods ** 301 | ***************************************/ 302 | 303 | /** 304 | * @brief Invoke a lambda with an optional payload 305 | * @param ggreq Provides context about the request 306 | * @param opts Describes the options for invoke 307 | * @param result Describes the result of the request 308 | * @return Greengrass error code 309 | */ 310 | gg_error gg_invoke(gg_request ggreq, const gg_invoke_options *opts, 311 | gg_request_result *result); 312 | 313 | /*************************************** 314 | ** AWS IoT Methods ** 315 | ***************************************/ 316 | 317 | /** 318 | * @brief Initialize the publish options 319 | * @param opts Pointer to publish options to be initialized 320 | * @return Greengrass error code 321 | * @note Need to call gg_publish_options_free on opts when done using it 322 | */ 323 | gg_error gg_publish_options_init(gg_publish_options *opts); 324 | 325 | /** 326 | * @brief Free a publish options that was created by gg_publish_options_init 327 | * @param opts Publish options to be freed 328 | * @return Greengrass error code 329 | */ 330 | gg_error gg_publish_options_free(gg_publish_options opts); 331 | 332 | /** 333 | * @brief Sets the queue full policy on a publish options 334 | * @param opts Publish options to be configured 335 | * @param policy Selected queue full policy to be set 336 | * @return Greengrass error code 337 | */ 338 | gg_error gg_publish_options_set_queue_full_policy(gg_publish_options opts, 339 | gg_queue_full_policy_options policy); 340 | 341 | /** 342 | * @brief Publish a payload to a topic 343 | * @param ggreq Provides context about the request 344 | * @param topic Null-terminated string topic where to publish the payload 345 | * @param payload Data to be sent to the topic - caller will free 346 | * @param payload_size Size of payload buffer 347 | * @param opts Publish options that configure publish behavior 348 | * @param result Describes the result of the request 349 | * @return Greengrass error code 350 | */ 351 | gg_error gg_publish_with_options(gg_request ggreq, const char *topic, 352 | const void *payload, size_t payload_size, const gg_publish_options opts, 353 | gg_request_result *result); 354 | 355 | /** 356 | * @brief Publish a payload to a topic 357 | * @param ggreq Provides context about the request 358 | * @param topic Null-terminated string topic where to publish the payload 359 | * @param payload Data to be sent to the topic - caller will free 360 | * @param payload_size Size of payload buffer 361 | * @param result Describes the result of the request 362 | * @return Greengrass error code 363 | * @note Calls gg_publish_with_options with opts==NULL 364 | */ 365 | gg_error gg_publish(gg_request ggreq, const char *topic, const void *payload, 366 | size_t payload_size, gg_request_result *result); 367 | 368 | /** 369 | * @brief Get thing shadow for thing name 370 | * @param ggreq Provides context about the request 371 | * @param thing_name Null-terminated string specifying thing shadow to get 372 | * @param result Describes the result of the request 373 | * @return Greengrass error code 374 | */ 375 | gg_error gg_get_thing_shadow(gg_request ggreq, const char *thing_name, 376 | gg_request_result *result); 377 | 378 | /** 379 | * @brief Update thing shadow for thing name 380 | * @param ggreq Provides context about the request 381 | * @param thing_name Null-terminated string specifying thing shadow to update 382 | * @param update_payload Null-terminated string to be updated in the shadow 383 | * @param result Describes the result of the request 384 | * @return Greengrass error code 385 | */ 386 | gg_error gg_update_thing_shadow(gg_request ggreq, const char *thing_name, 387 | const char *update_payload, 388 | gg_request_result *result); 389 | 390 | /** 391 | * @brief Delete thing shadow for thing name 392 | * @param ggreq Provides context about the request 393 | * @param thing_name Null-terminated string specifying thing shadow to delete 394 | * @param thing_name Specifies which thing shadow should be deleted 395 | * @param result Describes the result of the request 396 | * @return Greengrass error code 397 | */ 398 | gg_error gg_delete_thing_shadow(gg_request ggreq, const char *thing_name, 399 | gg_request_result *result); 400 | 401 | #ifdef __cplusplus 402 | } 403 | #endif 404 | 405 | #endif /* #ifndef _GREENGRASS_SDK_H_ */ 406 | -------------------------------------------------------------------------------- /stubs/src/greengrasssdk.c: -------------------------------------------------------------------------------- 1 | #include "shared/greengrasssdk.h" 2 | 3 | gg_error gg_global_init(uint32_t opt) { 4 | return GGE_SUCCESS; 5 | } 6 | 7 | gg_error gg_log(gg_log_level level, const char *format, ...) 8 | { 9 | return GGE_SUCCESS; 10 | } 11 | 12 | gg_error gg_request_init(gg_request *ggreq) 13 | { 14 | return GGE_SUCCESS; 15 | } 16 | 17 | gg_error gg_request_close(gg_request ggreq) 18 | { 19 | return GGE_SUCCESS; 20 | } 21 | 22 | gg_error gg_request_read(gg_request ggreq, void *buffer, size_t buffer_size, 23 | size_t *amount_read) { 24 | 25 | return GGE_SUCCESS; 26 | } 27 | 28 | gg_error gg_runtime_start(gg_lambda_handler handler, uint32_t opt) 29 | { 30 | return GGE_SUCCESS; 31 | } 32 | 33 | gg_error gg_lambda_handler_read(void *buffer, size_t buffer_size, 34 | size_t *amount_read) 35 | { 36 | return GGE_SUCCESS; 37 | } 38 | gg_error gg_lambda_handler_write_response(const void *response, 39 | size_t response_size) 40 | { 41 | return GGE_SUCCESS; 42 | } 43 | 44 | gg_error gg_lambda_handler_write_error(const char *error_message) 45 | { 46 | return GGE_SUCCESS; 47 | } 48 | 49 | gg_error gg_get_secret_value(gg_request ggreq, const char *secret_id, 50 | const char *version_id, const char *version_stage, 51 | gg_request_result *result) 52 | { 53 | return GGE_SUCCESS; 54 | } 55 | 56 | gg_error gg_invoke(gg_request ggreq, const gg_invoke_options *opts, 57 | gg_request_result *result) 58 | { 59 | return GGE_SUCCESS; 60 | } 61 | 62 | gg_error gg_publish_options_init(gg_publish_options *opts) 63 | { 64 | return GGE_SUCCESS; 65 | } 66 | 67 | gg_error gg_publish_options_free(gg_publish_options opts) 68 | { 69 | return GGE_SUCCESS; 70 | } 71 | 72 | gg_error gg_publish_options_set_queue_full_policy(gg_publish_options opts, 73 | gg_queue_full_policy_options policy) 74 | { 75 | return GGE_SUCCESS; 76 | } 77 | 78 | gg_error gg_publish_with_options(gg_request ggreq, const char *topic, 79 | const void *payload, size_t payload_size, const gg_publish_options opts, 80 | gg_request_result *result) 81 | { 82 | return GGE_SUCCESS; 83 | } 84 | 85 | gg_error gg_publish(gg_request ggreq, const char *topic, const void *payload, 86 | size_t payload_size, gg_request_result *result) 87 | { 88 | return GGE_SUCCESS; 89 | } 90 | 91 | gg_error gg_get_thing_shadow(gg_request ggreq, const char *thing_name, 92 | gg_request_result *result) 93 | { 94 | return GGE_SUCCESS; 95 | } 96 | 97 | gg_error gg_delete_thing_shadow(gg_request ggreq, const char *thing_name, 98 | gg_request_result *result) 99 | { 100 | return GGE_SUCCESS; 101 | } 102 | 103 | gg_error gg_update_thing_shadow(gg_request ggreq, const char *thing_name, 104 | const char *update_payload, 105 | gg_request_result *result) 106 | { 107 | return GGE_SUCCESS; 108 | } -------------------------------------------------------------------------------- /wrapper.h: -------------------------------------------------------------------------------- 1 | // This file is used by build.rs in generating bindings 2 | #include --------------------------------------------------------------------------------