Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Rust templates #160

Merged
merged 33 commits into from Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
32d8944
feat: add Rust templates
nmoutschen Dec 13, 2021
49decd0
feat: small changes following PR review
nmoutschen Dec 15, 2021
95720bc
docs: add MSRV for Rust templates
nmoutschen Jan 8, 2022
ab55bae
chore: bump Rust AWS SDK to 0.4
nmoutschen Jan 11, 2022
76b37e8
feat(rust): merge hello world and DynamoDB templates
nmoutschen Jan 12, 2022
7235357
fix(rust): fix template and remove generic
nmoutschen Jan 12, 2022
7d1bac0
fix(rust): fix issues in manifest and test
nmoutschen Jan 12, 2022
5c23893
Apply suggestions from code review
nmoutschen Jan 13, 2022
c1edd7b
Merge branch 'master' into rust-templates
nmoutschen Jan 22, 2022
7851a05
feat(rust): rename buildspace file
nmoutschen Jan 27, 2022
1a6e702
Merge branch 'master' into rust-templates
nmoutschen Jan 27, 2022
4da8c14
chore(rust): bump AWS SDK
nmoutschen Jan 27, 2022
a9be397
Merge branch 'rust-templates' of github.com:nmoutschen/aws-sam-cli-ap…
nmoutschen Jan 27, 2022
4ab6494
chore(rust): bump SDK to 0.6
nmoutschen Jan 28, 2022
8e62352
chore(rust): cargo fmt
nmoutschen Jan 28, 2022
9a51052
Merge branch 'master' into rust-templates
nmoutschen Feb 8, 2022
dfd77eb
feat(rust(provided.al2)): rename 'rust' to 'rust(provided.al2)'
nmoutschen Feb 12, 2022
0402317
Merge branch 'master' into rust-templates
nmoutschen Feb 12, 2022
be506da
feat(rust): bump Lambda runtime to 0.5
nmoutschen Feb 21, 2022
44ffc66
feat(rust): use cargo-zigbuild for cross-compilation
nmoutschen Feb 21, 2022
1abbddd
chore(rust): bump AWS SDK to 0.7
nmoutschen Feb 21, 2022
5aaa0e9
chore(rust): bump AWS SDK
nmoutschen Feb 25, 2022
0eaad86
chore(rust): rename folder to 'rust (provided.al2)'
nmoutschen Feb 25, 2022
6a1aa83
chore(rust): rename folder to 'rust'
nmoutschen Feb 25, 2022
d34041c
Merge branch 'master' into rust-templates
nmoutschen Feb 25, 2022
79d1757
Merge branch 'master' into rust-templates
nmoutschen Mar 2, 2022
281aca2
docs(rust): set MSRV to 1.56.0 or newer
nmoutschen Apr 7, 2022
996b656
Merge branch 'master' into rust-templates
mndeveci Apr 7, 2022
9360bdb
fix(rust): move to manifest-v2
nmoutschen Apr 7, 2022
a804d68
chore(provided.al2/rust): move rust to provided.al2/rust
nmoutschen Apr 14, 2022
2bd9766
Merge branch 'master' into rust-templates
nmoutschen Apr 14, 2022
4736eb0
fix(provided.al2/rust): source cargo in unittest buildspec
nmoutschen Apr 20, 2022
07578b5
fix(provided.al2): fix unit test
nmoutschen Apr 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions buildspec.yml
Expand Up @@ -46,6 +46,8 @@ batch:
buildspec: buildspecs/buildspec-unittest-java11.yml
- identifier: UnitTestRuby27
buildspec: buildspecs/buildspec-unittest-ruby2.7.yml
- identifier: UnitTestRust
buildspec: buildspecs/buildspec-unittest-rust.yml
- identifier: BuildAndInvoke
buildspec: buildspecs/buildspec-build-invoke.yml
env:
Expand Down
20 changes: 20 additions & 0 deletions buildspecs/buildspec-unittest-rust.yml
@@ -0,0 +1,20 @@
version: 0.2

phases:
install:
commands:
- pip install --upgrade pip aws-sam-cli
- pip install -r requirements.txt
# Install rustup
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# Configure cargo for the current shell
- source $HOME/.cargo/env
# Install cross for cross-compilation
- cargo install cross

post_build:
commands:
- sam --version
# sam build doesn't support building Rust Lambda functions
# - pytest -vvv tests/integration/build_invoke/test_build_invoke_rust.py -n 4
- pytest -vvv tests/integration/unit_test/test_unit_test_rust.py -n 4
10 changes: 10 additions & 0 deletions manifest-v2.json
Expand Up @@ -645,6 +645,16 @@
"useCaseName": "Multi-step workflow"
}
],
"rust (provided.al2)": [
{
"directory": "provided.al2/rust/cookiecutter-aws-sam-hello-rust",
"displayName": "Hello World Example",
"dependencyManager": "cargo",
"appTemplate": "hello-world",
"packageType": "Zip",
"useCaseName": "Hello World Example"
}
],
"amazon/nodejs14.x-base": [
{
"directory": "nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image",
Expand Down
18 changes: 18 additions & 0 deletions provided.al2/rust/cookiecutter-aws-sam-hello-rust/.gitignore
@@ -0,0 +1,18 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

.aws-sam
build
samconfig.toml
@@ -0,0 +1,5 @@
{
"project_name": "My Project",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
"architecture": ["x86_64", "arm64"]
}
@@ -0,0 +1,18 @@
[package]
name = "{{ cookiecutter.project_slug }}"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
aws-config = "0.8"
aws-sdk-dynamodb = "0.8"
aws-smithy-client = { version = "0.38", features = ["test-util"] }
lambda_http = "0.5"
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
aws-smithy-http = "0.38"
aws-types = { version = "0.8", features = ["hardcoded-credentials"] }
http = "0.2"
@@ -0,0 +1,20 @@
{% if cookiecutter.architecture == 'arm64' -%}
ARCH = aarch64-unknown-linux-gnu
{%- else -%}
ARCH = x86_64-unknown-linux-gnu
{%- endif %}
ARCH_SPLIT = $(subst -, ,$(ARCH))

.PHONY: build
build:
ifeq ("$(shell zig targets | jq -r .native.cpu.arch)-$(shell zig targets | jq -r .native.os)-$(shell zig targets | jq -r .native.abi)", "$(word 1,$(ARCH_SPLIT))-$(word 3,$(ARCH_SPLIT))-$(word 4,$(ARCH_SPLIT))")
@echo "Same host and target. Using native build"
cargo build --release --target $(ARCH)
else
@echo "Different host and target. Using zigbuild"
cargo zigbuild --release --target $(ARCH)
endif

rm -rf ./build
mkdir -p ./build
cp -v ./target/$(ARCH)/release/{{ cookiecutter.project_slug }} ./build/bootstrap
@@ -0,0 +1,90 @@
# {{ cookiecutter.project_name }}

This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders:

- `Cargo.toml` - Project configuration file.
- `src` - Code for the application's Lambda function.
- `template.yaml` - A template that defines the application's AWS resources.

The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.

If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.

* [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)

## Deploy the sample application

To deploy the application, you need the folllowing tools:

* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community)
* [Rust](https://www.rust-lang.org/) version 1.56.0 or newer
* [cargo-zigbuild](https://github.com/messense/cargo-zigbuild) and [Zig](https://ziglang.org/) for cross-compilation
* [jq](https://stedolan.github.io/jq/) for tooling specific to this project

To build and deploy your application for the first time, run the following in your shell:

```bash
make build
sam deploy --guided
```

The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:

* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
* **AWS Region**: The AWS region you want to deploy your app to.
* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
* **Save arguments to `samconfig.toml`**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.

You can find your API Gateway Endpoint URL in the output values displayed after deployment.


## Add a resource to your application
The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types.

## Fetch, tail, and filter Lambda function logs

To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug.

`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM.

```bash
{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail
```

You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html).

## Tests

Tests are defined alongside your lambda function code in the `src` folder.

```bash
cargo test
```


## Cleanup

To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following:

```bash
sam delete
```

## Resources

See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts.

Next, you can use AWS Serverless Application Repository to deploy ready-to-use apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/).
@@ -0,0 +1,148 @@
use aws_sdk_dynamodb::{model::AttributeValue, Client};
use lambda_http::{service_fn, Body, Error, IntoResponse, Request, RequestExt, Response};
use std::env;

/// Main function
#[tokio::main]
async fn main() -> Result<(), Error> {
// Initialize the AWS SDK for Rust
let config = aws_config::load_from_env().await;
let table_name = env::var("TABLE_NAME").expect("TABLE_NAME must be set");
let dynamodb_client = Client::new(&config);

// Register the Lambda handler
//
// We use a closure to pass the `dynamodb_client` and `table_name` as arguments
// to the handler function.
lambda_http::run(service_fn(|request: Request| {
put_item(&dynamodb_client, &table_name, request)
}))
.await?;

Ok(())
}

/// Put Item Lambda function
///
/// This function will run for every invoke of the Lambda function.
async fn put_item(
client: &Client,
table_name: &str,
request: Request,
) -> Result<impl IntoResponse, Error> {
// Extract path parameter from request
let path_parameters = request.path_parameters();
let id = match path_parameters.first("id") {
Some(id) => id,
None => return Ok(Response::builder().status(400).body("id is required")?),
};

// Extract body from request
let body = match request.body() {
Body::Empty => "".to_string(),
Body::Text(body) => body.clone(),
Body::Binary(body) => String::from_utf8_lossy(body).to_string(),
};

// Put the item in the DynamoDB table
let res = client
.put_item()
.table_name(table_name)
.item("id", AttributeValue::S(id.to_string()))
.item("payload", AttributeValue::S(body))
.send()
.await;

// Return a response to the end-user
match res {
Ok(_) => Ok(Response::builder().status(200).body("item saved")?),
Err(_) => Ok(Response::builder().status(500).body("internal error")?),
}
}

/// Unit tests
///
/// These tests are run using the `cargo test` command.
#[cfg(test)]
mod tests {
use super::*;
use aws_sdk_dynamodb::{Client, Config, Credentials, Region};
use aws_smithy_client::{erase::DynConnector, test_connection::TestConnection};
use aws_smithy_http::body::SdkBody;
use std::collections::HashMap;

// Helper function to create a mock AWS configuration
async fn get_mock_config() -> Config {
let cfg = aws_config::from_env()
.region(Region::new("eu-west-1"))
.credentials_provider(Credentials::new(
"access_key",
"privatekey",
None,
None,
"dummy",
))
.load()
.await;

Config::new(&cfg)
}

/// Helper function to generate a sample DynamoDB request
fn get_request_builder() -> http::request::Builder {
http::Request::builder()
.header("content-type", "application/x-amz-json-1.0")
.uri(http::uri::Uri::from_static(
"https://dynamodb.eu-west-1.amazonaws.com/",
))
}

#[tokio::test]
async fn test_put_item() {
// Mock DynamoDB client
//
// `TestConnection` takes a vector of requests and responses, allowing us to
// simulate the behaviour of the DynamoDB API endpoint. Since we are only
// making a single request in this test, we only need to provide a single
// entry in the vector.
let conn = TestConnection::new(vec![(
get_request_builder()
.header("x-amz-target", "DynamoDB_20120810.PutItem")
.body(SdkBody::from(
r#"{"TableName":"test","Item":{"id":{"S":"1"},"payload":{"S":"test1"}}}"#,
))
.unwrap(),
http::Response::builder()
.status(200)
.body(SdkBody::from(
r#"{"Attributes": {"id": {"S": "1"}, "payload": {"S": "test1"}}}"#,
))
.unwrap(),
)]);
let client =
Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone()));

let table_name = "test_table";

// Mock API Gateway request
let mut path_parameters = HashMap::new();
path_parameters.insert("id".to_string(), vec!["1".to_string()]);

let request = http::Request::builder()
.method("PUT")
.uri("/1")
.body(Body::Text("test1".to_string()))
.unwrap()
.with_path_parameters(path_parameters);

// Send mock request to Lambda handler function
let response = put_item(&client, table_name, request)
.await
.unwrap()
.into_response();

// Assert that the response is correct
assert_eq!(response.status(), 200);
assert_eq!(response.body(), &Body::Text("item saved".to_string()));
}
}