From 905a5f75c5a5352366a93fe63d67893813a4455c Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Sun, 1 Jan 2023 22:19:30 +0000 Subject: [PATCH 1/8] Add an opening quickstart with an example. Getting the example to compile was an absolute nightmare. Happy New Year! --- .devcontainer/dev-setup.sh | 7 ++++++ Cargo.toml | 1 + src/lib.rs | 51 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/.devcontainer/dev-setup.sh b/.devcontainer/dev-setup.sh index 648e258..6218fc4 100644 --- a/.devcontainer/dev-setup.sh +++ b/.devcontainer/dev-setup.sh @@ -1,6 +1,13 @@ sudo apt-get update sudo apt-get upgrade -y + +# Cross-compiling to x86_64-unknown-linux-musl. sudo apt-get install -y musl-tools rustup target add x86_64-unknown-linux-musl + +# Compiling doctests requires nightly (https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#doctest-xcompile). +rustup toolchain install nightly +rustup target add --toolchain nightly x86_64-unknown-linux-musl + cargo fetch cargo install cargo-deny diff --git a/Cargo.toml b/Cargo.toml index 4f58d73..de3a518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ httpmock = "0.6" maplit = "1" temp-env = "0.3" tokio = { version = "1.23.0", features = ["macros"] } +tokio-test = "0.4" diff --git a/src/lib.rs b/src/lib.rs index 4dc9937..498d128 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,54 @@ +//! Cache AWS Secrets Manager secrets in your AWS Lambda function, reducing latency (we don't need to query another service) and cost ([Secrets Manager charges based on queries]). +//! +//! # Quickstart +//! Add the [AWS Parameters and Secrets Lambda Extension] [layer to your Lambda function]. Only version 2 of this layer is currently supported. +//! +//! Assuming a secret exists with the name "backend-server" containing a key/value pair with a key of "api_key" and a value of +//! "dd96eeda-16d3-4c86-975f-4986e603ec8c" (our super secret API key to our backend), this code will get the secret from the cache, querying +//! Secrets Manager if it is not in the cache, and present it in a strongly-typed `BackendServer` object. +//! +//! ```rust +//! use aws_parameters_and_secrets_lambda::Manager; +//! use serde::Deserialize; +//! +//! #[derive(Deserialize)] +//! struct BackendServer { +//! api_key: String +//! } +//! +//! # let server = httpmock::MockServer::start(); +//! # let mock = server.mock(|when, then| { +//! # when.method("GET").path("/secretsmanager/get"); +//! # then.status(200).body("{\"SecretString\": \"{\\\"api_key\\\": \\\"dd96eeda-16d3-4c86-975f-4986e603ec8c\\\"}\"}"); +//! # }); +//! # +//! # temp_env::with_vars( +//! # vec![ +//! # ("AWS_SESSION_TOKEN", Some("xyz")), +//! # ("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT", Some(&server.port().to_string())) +//! # ], +//! # || { +//! # tokio_test::block_on( +//! # std::panic::AssertUnwindSafe( +//! # async { +//! let manager = Manager::default(); +//! let secret = manager.get_secret("backend-server"); +//! let secret_value: BackendServer = secret.get_typed().await?; +//! assert_eq!("dd96eeda-16d3-4c86-975f-4986e603ec8c", secret_value.api_key); +//! # Ok::<_, anyhow::Error>(()) +//! # } +//! # ) +//! # ); +//! # } +//! # ); +//! # +//! # mock.assert(); +//! ``` +//! +//! [Secrets Manager charges based on queries]: https://aws.amazon.com/secrets-manager/pricing/ +//! [AWS Parameters and Secrets Lambda Extension]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html +//! [layer to your Lambda function]: https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html + use std::fmt::Debug; use std::{env, sync::Arc}; From d238c6af622c137b21803ec6c974e7219dd2786a Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Sun, 1 Jan 2023 23:37:58 +0000 Subject: [PATCH 2/8] Document Manager. --- src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 498d128..c795499 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,12 +127,18 @@ impl Default for ManagerBuilder { } } +/// Manages connections to the cache. +/// +/// Ideally, only one of these should exist in a single executable (cloning is fine as it will reuse the connections). #[derive(Debug, Clone)] pub struct Manager { connection: Arc, } impl Manager { + /// Get a representation of a secret that matches a given query. + /// + /// Note that this does not return the value of the secret; see [`Secret`] for how to get it. pub fn get_secret(&self, query: impl Query) -> Secret { Secret { query: query.get_query_string(), @@ -142,6 +148,11 @@ impl Manager { } impl Default for Manager { + /// Initialise a default `Manager` from the environment. + /// + /// # Panics + /// If the AWS Lambda environment is invalid, this will panic. + /// It is strongly recommended to use a [`ManagerBuilder`] instead as it is more flexible and has proper error handling. fn default() -> Self { ManagerBuilder::new().build().unwrap() } From d025098f0763edb6b190bf771ff4d93f21c9ccf1 Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 00:22:19 +0000 Subject: [PATCH 3/8] Document ManagerBuilder. --- src/lib.rs | 54 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c795499..b130bb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,27 @@ //! Cache AWS Secrets Manager secrets in your AWS Lambda function, reducing latency (we don't need to query another service) and cost ([Secrets Manager charges based on queries]). -//! +//! //! # Quickstart //! Add the [AWS Parameters and Secrets Lambda Extension] [layer to your Lambda function]. Only version 2 of this layer is currently supported. -//! -//! Assuming a secret exists with the name "backend-server" containing a key/value pair with a key of "api_key" and a value of +//! +//! Assuming a secret exists with the name "backend-server" containing a key/value pair with a key of "api_key" and a value of //! "dd96eeda-16d3-4c86-975f-4986e603ec8c" (our super secret API key to our backend), this code will get the secret from the cache, querying -//! Secrets Manager if it is not in the cache, and present it in a strongly-typed `BackendServer` object. -//! +//! Secrets Manager if it is not in the cache, and present it in a strongly-typed `BackendServer` object. +//! //! ```rust //! use aws_parameters_and_secrets_lambda::Manager; //! use serde::Deserialize; -//! +//! //! #[derive(Deserialize)] //! struct BackendServer { //! api_key: String //! } -//! +//! //! # let server = httpmock::MockServer::start(); //! # let mock = server.mock(|when, then| { //! # when.method("GET").path("/secretsmanager/get"); //! # then.status(200).body("{\"SecretString\": \"{\\\"api_key\\\": \\\"dd96eeda-16d3-4c86-975f-4986e603ec8c\\\"}\"}"); //! # }); -//! # +//! # //! # temp_env::with_vars( //! # vec![ //! # ("AWS_SESSION_TOKEN", Some("xyz")), @@ -44,7 +44,7 @@ //! # //! # mock.assert(); //! ``` -//! +//! //! [Secrets Manager charges based on queries]: https://aws.amazon.com/secrets-manager/pricing/ //! [AWS Parameters and Secrets Lambda Extension]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html //! [layer to your Lambda function]: https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html @@ -68,6 +68,17 @@ assert_impl_all!(Secret: Send, Sync, Debug, Clone); assert_impl_all!(VersionIdQuery: Send, Sync, Debug, Clone); assert_impl_all!(VersionStageQuery: Send, Sync, Debug, Clone); +/// Flexible builder for a [`Manager`]. +/// +/// This sample should be all you ever need to use. It is identical to [`Manager::default`](struct.Manager.html#method.default) but does not panic on failure. +/// +/// ```rust +/// # use aws_parameters_and_secrets_lambda::ManagerBuilder; +/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || { +/// let manager = ManagerBuilder::new().build()?; +/// # Ok::<_, anyhow::Error>(()) +/// # }); +/// ``` #[derive(Debug)] #[must_use = "construct a `Manager` with the `build` method"] pub struct ManagerBuilder { @@ -76,6 +87,8 @@ pub struct ManagerBuilder { } impl ManagerBuilder { + /// Create a new builder with the default values. + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { port: None, @@ -83,16 +96,25 @@ impl ManagerBuilder { } } + /// Use the given port for the extension server instead of the default. + /// + /// If this is not called before [`build`](Self::build), then the "PARAMETERS_SECRETS_EXTENSION_HTTP_PORT" + /// environment variable will be used, or 2773 if this is not set. pub fn with_port(mut self, port: u16) -> Self { self.port = Some(port); self } + /// Use the given token to authenticate with the extension server instead of the default. + /// + /// If this is not called before [`build`](Self::build), then the "AWS_SESSION_TOKEN" + /// environment variable will be used. pub fn with_token(mut self, token: String) -> Self { self.token = Some(token); self } + /// Create a [`Manager`] from the given values. pub fn build(self) -> Result { let port = match self.port { Some(port) => port, @@ -121,14 +143,8 @@ impl ManagerBuilder { } } -impl Default for ManagerBuilder { - fn default() -> Self { - Self::new() - } -} - -/// Manages connections to the cache. -/// +/// Manages connections to the cache. Create one via a [`ManagerBuilder`]. +/// /// Ideally, only one of these should exist in a single executable (cloning is fine as it will reuse the connections). #[derive(Debug, Clone)] pub struct Manager { @@ -137,7 +153,7 @@ pub struct Manager { impl Manager { /// Get a representation of a secret that matches a given query. - /// + /// /// Note that this does not return the value of the secret; see [`Secret`] for how to get it. pub fn get_secret(&self, query: impl Query) -> Secret { Secret { @@ -149,7 +165,7 @@ impl Manager { impl Default for Manager { /// Initialise a default `Manager` from the environment. - /// + /// /// # Panics /// If the AWS Lambda environment is invalid, this will panic. /// It is strongly recommended to use a [`ManagerBuilder`] instead as it is more flexible and has proper error handling. From ab721180a8d3264a28087a2cdf4e7918d69e7551 Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 11:27:18 +0000 Subject: [PATCH 4/8] Document Query and related items. --- src/lib.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b130bb7..6e35852 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,21 +245,29 @@ struct ExtensionResponse { secret_string: String, } +/// A query for a specific [`Secret`] in AWS Secrets Manager. See [`Manager::get_secret`] for usage. +/// +/// # Sealed +/// You cannot implement this trait yourself. #[sealed] pub trait Query { + #[doc(hidden)] fn get_query_string(&self) -> String; } +/// Flexible builder for a complex [`Query`]. #[must_use = "continue building a query with the `with_version_id` or `with_version_stage` method"] pub struct QueryBuilder<'a> { secret_id: &'a str, } impl<'a> QueryBuilder<'a> { + /// Create a new builder with the secret name or ARN. pub fn new(secret_id: &'a str) -> Self { Self { secret_id } } + /// Create a query with a version id. pub fn with_version_id(self, version_id: &'a str) -> VersionIdQuery<'a> { VersionIdQuery { secret_id: self.secret_id, @@ -267,6 +275,7 @@ impl<'a> QueryBuilder<'a> { } } + /// Create a query with a version stage. pub fn with_version_stage(self, version_stage: &'a str) -> VersionStageQuery<'a> { VersionStageQuery { secret_id: self.secret_id, @@ -275,6 +284,20 @@ impl<'a> QueryBuilder<'a> { } } +/// Query by the secret name or ARN. +/// +/// This returns the current value of the secret (stage = "AWSCURRENT") and is usually what you want to use. +/// +/// Any string-like type can be used, including [`String`], [`&str`], and [`std::borrow::Cow`]. +/// +/// ```rust +/// # use aws_parameters_and_secrets_lambda::ManagerBuilder; +/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || { +/// # let manager = ManagerBuilder::new().build()?; +/// let secret = manager.get_secret("secret-name"); +/// # Ok::<_, anyhow::Error>(()) +/// # }); +/// ``` #[sealed] impl> Query for T { fn get_query_string(&self) -> String { @@ -282,12 +305,28 @@ impl> Query for T { } } +/// A query for a secret with a version id. Create one via [`QueryBuilder::with_version_id`]. +/// +/// The version id is a unique identifier returned by Secrets Manager when a secret is created or updated. #[derive(Debug, Clone)] pub struct VersionIdQuery<'a> { secret_id: &'a str, version_id: &'a str, } +/// Query by the version id of the secret as well as the secret name or ARN. +/// +/// ```rust +/// # use aws_parameters_and_secrets_lambda::ManagerBuilder; +/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || { +/// # let manager = ManagerBuilder::new().build()?; +/// use aws_parameters_and_secrets_lambda::QueryBuilder; +/// +/// let query = QueryBuilder::new("secret-name") +/// .with_version_id("18b94218-543d-4d67-aec5-f8e6a41f7813"); +/// let secret = manager.get_secret(query); +/// # Ok::<_, anyhow::Error>(()) +/// # }); #[sealed] impl Query for VersionIdQuery<'_> { fn get_query_string(&self) -> String { @@ -295,12 +334,29 @@ impl Query for VersionIdQuery<'_> { } } +/// A query for a secret with a version stage. Create one via [`QueryBuilder::with_version_stage`]. +/// +/// The "AWSCURRENT" stage is the current value of the secret, while the "AWSPREVIOUS" stage is the last value of the "AWSCURRENT" stage. +/// You can also use your own stages. #[derive(Debug, Clone)] pub struct VersionStageQuery<'a> { secret_id: &'a str, version_stage: &'a str, } +/// Query by the stage of the secret as well as the secret name or ARN. +/// +/// ```rust +/// # use aws_parameters_and_secrets_lambda::ManagerBuilder; +/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || { +/// # let manager = ManagerBuilder::new().build()?; +/// use aws_parameters_and_secrets_lambda::QueryBuilder; +/// +/// let query = QueryBuilder::new("secret-name") +/// .with_version_stage("AWSPREVIOUS"); +/// let secret = manager.get_secret(query); +/// # Ok::<_, anyhow::Error>(()) +/// # }); #[sealed] impl Query for VersionStageQuery<'_> { fn get_query_string(&self) -> String { From c1261a8bd1a3adda3e6656caf9dbd3287a6f877b Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 19:32:07 +0000 Subject: [PATCH 5/8] Document Secret. --- src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 6e35852..3437d8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,7 @@ impl Connection { } } +/// A representation of a secret in Secrets Manager. #[derive(Debug, Clone)] pub struct Secret { query: String, @@ -207,10 +208,14 @@ pub struct Secret { } impl Secret { + /// Get the plaintext value of this secret. + /// + /// Usually, this is in json format, but it can be any data format that you provide to Secrets Manager. pub async fn get_raw(&self) -> Result { self.connection.get_secret(&self.query).await } + /// Get a value by name from within this secret. pub async fn get_single(&self, name: impl AsRef) -> Result { let raw = &self.get_raw().await?; let name = name.as_ref(); @@ -225,6 +230,7 @@ impl Secret { Ok(String::from(secret)) } + /// Get the value of this secret, represented as a strongly-typed T. pub async fn get_typed(&self) -> Result { let raw = self.get_raw().await?; Ok(serde_json::from_str(&raw)?) From 3de75df87e7c6ac04a8cd78aaaccc3019733befe Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 19:32:51 +0000 Subject: [PATCH 6/8] Add deny missing docs lint. --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 3437d8d..1e2b1c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,8 @@ //! [AWS Parameters and Secrets Lambda Extension]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html //! [layer to your Lambda function]: https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html +#![deny(missing_docs)] + use std::fmt::Debug; use std::{env, sync::Arc}; From b0644337d2c9db4ba24d358f6f31de11140dfce0 Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 19:39:20 +0000 Subject: [PATCH 7/8] Add doctest action. --- .github/workflows/test.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6087e15..666d65d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,6 +76,29 @@ jobs: key: dependencies-${{ hashFiles('**/Cargo.toml') }} restore-keys: dependencies- - run: cargo test + doctest: + name: Documentation tests + runs-on: ubuntu-latest + needs: + - compile + steps: + - uses: actions/checkout@v3 + - run: sudo apt-get install -y musl-tools + - uses: ATiltedTree/setup-rust@v1 + with: + rust-version: nightly # https://github.com/rust-lang/cargo/issues/7040 + targets: x86_64-unknown-linux-musl + - uses: actions/cache/restore@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: dependencies-${{ hashFiles('**/Cargo.toml') }} + restore-keys: dependencies- + - run: cargo +nightly test -Zdoctest-xcompile deny: name: Lint dependencies runs-on: ubuntu-latest From b80796f5b08b23295ec44b625a9418d45f64c006 Mon Sep 17 00:00:00 2001 From: Tom Humphreys Date: Mon, 2 Jan 2023 19:39:33 +0000 Subject: [PATCH 8/8] Formatting. --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 666d65d..dd81869 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,8 +103,8 @@ jobs: name: Lint dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: EmbarkStudios/cargo-deny-action@v1 - with: - log-level: warn - command: check + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + log-level: warn + command: check