Skip to content

Commit b7f198d

Browse files
sumagowdaSuma Gowda
andauthored
Example that uses Axum and Diesel to connect to a PostgreSQL database with SSLmode ON (#682)
Co-authored-by: Suma Gowda <[email protected]>
1 parent 6c3272f commit b7f198d

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed
6 KB
Binary file not shown.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "http-axum-diesel"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
7+
# Use cargo-edit(https://github.com/killercup/cargo-edit#installation)
8+
# to manage dependencies.
9+
# Running `cargo add DEPENDENCY_NAME` will
10+
# add the latest version of a dependency to the list,
11+
# and it will keep the alphabetic ordering for you.
12+
13+
[dependencies]
14+
axum = "0.6.4"
15+
bb8 = "0.8.0"
16+
diesel = "2.0.3"
17+
diesel-async = { version = "0.2.1", features = ["postgres", "bb8"] }
18+
lambda_http = { path = "../../lambda-http" }
19+
lambda_runtime = { path = "../../lambda-runtime" }
20+
serde = "1.0.159"
21+
tracing = { version = "0.1", features = ["log"] }
22+
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
23+
futures-util = "0.3.21"
24+
rustls = "0.20.8"
25+
rustls-native-certs = "0.6.2"
26+
tokio = { version = "1.2.0", default-features = false, features = ["macros", "rt-multi-thread"] }
27+
tokio-postgres = "0.7.7"
28+
tokio-postgres-rustls = "0.9.0"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# AWS Lambda Function example
2+
3+
This example shows how to develop a REST API with Axum and Diesel that connects to a Postgres database.
4+
5+
## Build & Deploy
6+
7+
1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation)
8+
2. Build the function with `cargo lambda build --release`
9+
3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE`
10+
11+
## Build for ARM 64
12+
13+
Build the function with `cargo lambda build --release --arm64`
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- This file should undo anything in `up.sql`
2+
DROP TABLE posts
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Your SQL goes here
2+
CREATE TABLE posts (
3+
id SERIAL PRIMARY KEY,
4+
title VARCHAR NOT NULL,
5+
content TEXT NOT NULL,
6+
published BOOLEAN NOT NULL DEFAULT FALSE
7+
)
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use diesel::{ConnectionError, ConnectionResult};
2+
use futures_util::future::BoxFuture;
3+
use futures_util::FutureExt;
4+
use std::time::Duration;
5+
6+
use axum::{
7+
extract::{Path, State},
8+
response::Json,
9+
routing::get,
10+
Router,
11+
};
12+
use bb8::Pool;
13+
use diesel::prelude::*;
14+
use diesel_async::{pooled_connection::AsyncDieselConnectionManager, AsyncPgConnection, RunQueryDsl};
15+
use lambda_http::{http::StatusCode, run, Error};
16+
use serde::{Deserialize, Serialize};
17+
18+
table! {
19+
posts (id) {
20+
id -> Integer,
21+
title -> Text,
22+
content -> Text,
23+
published -> Bool,
24+
}
25+
}
26+
27+
#[derive(Default, Queryable, Selectable, Serialize)]
28+
struct Post {
29+
id: i32,
30+
title: String,
31+
content: String,
32+
published: bool,
33+
}
34+
35+
#[derive(Deserialize, Insertable)]
36+
#[diesel(table_name = posts)]
37+
struct NewPost {
38+
title: String,
39+
content: String,
40+
published: bool,
41+
}
42+
43+
type AsyncPool = Pool<AsyncDieselConnectionManager<AsyncPgConnection>>;
44+
type ServerError = (StatusCode, String);
45+
46+
async fn create_post(State(pool): State<AsyncPool>, Json(post): Json<NewPost>) -> Result<Json<Post>, ServerError> {
47+
let mut conn = pool.get().await.map_err(internal_server_error)?;
48+
49+
let post = diesel::insert_into(posts::table)
50+
.values(post)
51+
.returning(Post::as_returning())
52+
.get_result(&mut conn)
53+
.await
54+
.map_err(internal_server_error)?;
55+
56+
Ok(Json(post))
57+
}
58+
59+
async fn list_posts(State(pool): State<AsyncPool>) -> Result<Json<Vec<Post>>, ServerError> {
60+
let mut conn = pool.get().await.map_err(internal_server_error)?;
61+
62+
let posts = posts::table
63+
.filter(posts::dsl::published.eq(true))
64+
.load(&mut conn)
65+
.await
66+
.map_err(internal_server_error)?;
67+
68+
Ok(Json(posts))
69+
}
70+
71+
async fn get_post(State(pool): State<AsyncPool>, Path(post_id): Path<i32>) -> Result<Json<Post>, ServerError> {
72+
let mut conn = pool.get().await.map_err(internal_server_error)?;
73+
74+
let post = posts::table
75+
.find(post_id)
76+
.first(&mut conn)
77+
.await
78+
.map_err(internal_server_error)?;
79+
80+
Ok(Json(post))
81+
}
82+
83+
async fn delete_post(State(pool): State<AsyncPool>, Path(post_id): Path<i32>) -> Result<(), ServerError> {
84+
let mut conn = pool.get().await.map_err(internal_server_error)?;
85+
86+
diesel::delete(posts::table.find(post_id))
87+
.execute(&mut conn)
88+
.await
89+
.map_err(internal_server_error)?;
90+
91+
Ok(())
92+
}
93+
94+
fn internal_server_error<E: std::error::Error>(err: E) -> ServerError {
95+
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
96+
}
97+
98+
#[tokio::main]
99+
async fn main() -> Result<(), Error> {
100+
// required to enable CloudWatch error logging by the runtime
101+
tracing_subscriber::fmt()
102+
.with_max_level(tracing::Level::INFO)
103+
// disable printing the name of the module in every log line.
104+
.with_target(false)
105+
// disabling time is handy because CloudWatch will add the ingestion time.
106+
.without_time()
107+
.init();
108+
109+
// Set up the database connection
110+
// Format for DATABASE_URL=postgres://your_username:your_password@your_host:5432/your_db?sslmode=require
111+
let db_url = std::env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set");
112+
113+
let mgr = AsyncDieselConnectionManager::<AsyncPgConnection>::new_with_setup(
114+
db_url,
115+
establish_connection,
116+
);
117+
118+
let pool = Pool::builder()
119+
.max_size(10)
120+
.min_idle(Some(5))
121+
.max_lifetime(Some(Duration::from_secs(60 * 60 * 24)))
122+
.idle_timeout(Some(Duration::from_secs(60 * 2)))
123+
.build(mgr)
124+
.await?;
125+
126+
// Set up the API routes
127+
let posts_api = Router::new()
128+
.route("/", get(list_posts).post(create_post))
129+
.route("/:id", get(get_post).delete(delete_post))
130+
.route("/get", get(list_posts))
131+
.route("/get/:id", get(get_post));
132+
let app = Router::new().nest("/posts", posts_api).with_state(pool);
133+
134+
run(app).await
135+
}
136+
137+
138+
fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConnection>> {
139+
let fut = async {
140+
// We first set up the way we want rustls to work.
141+
let rustls_config = rustls::ClientConfig::builder()
142+
.with_safe_defaults()
143+
.with_root_certificates(root_certs())
144+
.with_no_client_auth();
145+
let tls = tokio_postgres_rustls::MakeRustlsConnect::new(rustls_config);
146+
let (client, conn) = tokio_postgres::connect(config, tls)
147+
.await
148+
.map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
149+
tokio::spawn(async move {
150+
if let Err(e) = conn.await {
151+
eprintln!("Database connection: {e}");
152+
}
153+
});
154+
AsyncPgConnection::try_from(client).await
155+
};
156+
fut.boxed()
157+
}
158+
159+
fn root_certs() -> rustls::RootCertStore {
160+
let mut roots = rustls::RootCertStore::empty();
161+
let certs = rustls_native_certs::load_native_certs().expect("Certs not loadable!");
162+
let certs: Vec<_> = certs.into_iter().map(|cert| cert.0).collect();
163+
roots.add_parsable_certificates(&certs);
164+
roots
165+
}

0 commit comments

Comments
 (0)