Skip to content

Commit f86aece

Browse files
authored
Merge pull request #41 from Tom01098:Tom01098/issue6
Documentation
2 parents cb4bc62 + b80796f commit f86aece

File tree

4 files changed

+184
-11
lines changed

4 files changed

+184
-11
lines changed

.devcontainer/dev-setup.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
sudo apt-get update
22
sudo apt-get upgrade -y
3+
4+
# Cross-compiling to x86_64-unknown-linux-musl.
35
sudo apt-get install -y musl-tools
46
rustup target add x86_64-unknown-linux-musl
7+
8+
# Compiling doctests requires nightly (https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#doctest-xcompile).
9+
rustup toolchain install nightly
10+
rustup target add --toolchain nightly x86_64-unknown-linux-musl
11+
512
cargo fetch
613
cargo install cargo-deny

.github/workflows/test.yml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,35 @@ jobs:
7676
key: dependencies-${{ hashFiles('**/Cargo.toml') }}
7777
restore-keys: dependencies-
7878
- run: cargo test
79+
doctest:
80+
name: Documentation tests
81+
runs-on: ubuntu-latest
82+
needs:
83+
- compile
84+
steps:
85+
- uses: actions/checkout@v3
86+
- run: sudo apt-get install -y musl-tools
87+
- uses: ATiltedTree/setup-rust@v1
88+
with:
89+
rust-version: nightly # https://github.com/rust-lang/cargo/issues/7040
90+
targets: x86_64-unknown-linux-musl
91+
- uses: actions/cache/restore@v3
92+
with:
93+
path: |
94+
~/.cargo/bin/
95+
~/.cargo/registry/index/
96+
~/.cargo/registry/cache/
97+
~/.cargo/git/db/
98+
target/
99+
key: dependencies-${{ hashFiles('**/Cargo.toml') }}
100+
restore-keys: dependencies-
101+
- run: cargo +nightly test -Zdoctest-xcompile
79102
deny:
80103
name: Lint dependencies
81104
runs-on: ubuntu-latest
82105
steps:
83-
- uses: actions/checkout@v3
84-
- uses: EmbarkStudios/cargo-deny-action@v1
85-
with:
86-
log-level: warn
87-
command: check
106+
- uses: actions/checkout@v3
107+
- uses: EmbarkStudios/cargo-deny-action@v1
108+
with:
109+
log-level: warn
110+
command: check

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ httpmock = "0.6"
1818
maplit = "1"
1919
temp-env = "0.3"
2020
tokio = { version = "1.23.0", features = ["macros"] }
21+
tokio-test = "0.4"

src/lib.rs

Lines changed: 148 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,56 @@
1+
//! 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]).
2+
//!
3+
//! # Quickstart
4+
//! Add the [AWS Parameters and Secrets Lambda Extension] [layer to your Lambda function]. Only version 2 of this layer is currently supported.
5+
//!
6+
//! Assuming a secret exists with the name "backend-server" containing a key/value pair with a key of "api_key" and a value of
7+
//! "dd96eeda-16d3-4c86-975f-4986e603ec8c" (our super secret API key to our backend), this code will get the secret from the cache, querying
8+
//! Secrets Manager if it is not in the cache, and present it in a strongly-typed `BackendServer` object.
9+
//!
10+
//! ```rust
11+
//! use aws_parameters_and_secrets_lambda::Manager;
12+
//! use serde::Deserialize;
13+
//!
14+
//! #[derive(Deserialize)]
15+
//! struct BackendServer {
16+
//! api_key: String
17+
//! }
18+
//!
19+
//! # let server = httpmock::MockServer::start();
20+
//! # let mock = server.mock(|when, then| {
21+
//! # when.method("GET").path("/secretsmanager/get");
22+
//! # then.status(200).body("{\"SecretString\": \"{\\\"api_key\\\": \\\"dd96eeda-16d3-4c86-975f-4986e603ec8c\\\"}\"}");
23+
//! # });
24+
//! #
25+
//! # temp_env::with_vars(
26+
//! # vec![
27+
//! # ("AWS_SESSION_TOKEN", Some("xyz")),
28+
//! # ("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT", Some(&server.port().to_string()))
29+
//! # ],
30+
//! # || {
31+
//! # tokio_test::block_on(
32+
//! # std::panic::AssertUnwindSafe(
33+
//! # async {
34+
//! let manager = Manager::default();
35+
//! let secret = manager.get_secret("backend-server");
36+
//! let secret_value: BackendServer = secret.get_typed().await?;
37+
//! assert_eq!("dd96eeda-16d3-4c86-975f-4986e603ec8c", secret_value.api_key);
38+
//! # Ok::<_, anyhow::Error>(())
39+
//! # }
40+
//! # )
41+
//! # );
42+
//! # }
43+
//! # );
44+
//! #
45+
//! # mock.assert();
46+
//! ```
47+
//!
48+
//! [Secrets Manager charges based on queries]: https://aws.amazon.com/secrets-manager/pricing/
49+
//! [AWS Parameters and Secrets Lambda Extension]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html
50+
//! [layer to your Lambda function]: https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html
51+
52+
#![deny(missing_docs)]
53+
154
use std::fmt::Debug;
255
use std::{env, sync::Arc};
356

@@ -17,6 +70,17 @@ assert_impl_all!(Secret: Send, Sync, Debug, Clone);
1770
assert_impl_all!(VersionIdQuery: Send, Sync, Debug, Clone);
1871
assert_impl_all!(VersionStageQuery: Send, Sync, Debug, Clone);
1972

73+
/// Flexible builder for a [`Manager`].
74+
///
75+
/// 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.
76+
///
77+
/// ```rust
78+
/// # use aws_parameters_and_secrets_lambda::ManagerBuilder;
79+
/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || {
80+
/// let manager = ManagerBuilder::new().build()?;
81+
/// # Ok::<_, anyhow::Error>(())
82+
/// # });
83+
/// ```
2084
#[derive(Debug)]
2185
#[must_use = "construct a `Manager` with the `build` method"]
2286
pub struct ManagerBuilder {
@@ -25,23 +89,34 @@ pub struct ManagerBuilder {
2589
}
2690

2791
impl ManagerBuilder {
92+
/// Create a new builder with the default values.
93+
#[allow(clippy::new_without_default)]
2894
pub fn new() -> Self {
2995
Self {
3096
port: None,
3197
token: None,
3298
}
3399
}
34100

101+
/// Use the given port for the extension server instead of the default.
102+
///
103+
/// If this is not called before [`build`](Self::build), then the "PARAMETERS_SECRETS_EXTENSION_HTTP_PORT"
104+
/// environment variable will be used, or 2773 if this is not set.
35105
pub fn with_port(mut self, port: u16) -> Self {
36106
self.port = Some(port);
37107
self
38108
}
39109

110+
/// Use the given token to authenticate with the extension server instead of the default.
111+
///
112+
/// If this is not called before [`build`](Self::build), then the "AWS_SESSION_TOKEN"
113+
/// environment variable will be used.
40114
pub fn with_token(mut self, token: String) -> Self {
41115
self.token = Some(token);
42116
self
43117
}
44118

119+
/// Create a [`Manager`] from the given values.
45120
pub fn build(self) -> Result<Manager> {
46121
let port = match self.port {
47122
Some(port) => port,
@@ -70,18 +145,18 @@ impl ManagerBuilder {
70145
}
71146
}
72147

73-
impl Default for ManagerBuilder {
74-
fn default() -> Self {
75-
Self::new()
76-
}
77-
}
78-
148+
/// Manages connections to the cache. Create one via a [`ManagerBuilder`].
149+
///
150+
/// Ideally, only one of these should exist in a single executable (cloning is fine as it will reuse the connections).
79151
#[derive(Debug, Clone)]
80152
pub struct Manager {
81153
connection: Arc<Connection>,
82154
}
83155

84156
impl Manager {
157+
/// Get a representation of a secret that matches a given query.
158+
///
159+
/// Note that this does not return the value of the secret; see [`Secret`] for how to get it.
85160
pub fn get_secret(&self, query: impl Query) -> Secret {
86161
Secret {
87162
query: query.get_query_string(),
@@ -91,6 +166,11 @@ impl Manager {
91166
}
92167

93168
impl Default for Manager {
169+
/// Initialise a default `Manager` from the environment.
170+
///
171+
/// # Panics
172+
/// If the AWS Lambda environment is invalid, this will panic.
173+
/// It is strongly recommended to use a [`ManagerBuilder`] instead as it is more flexible and has proper error handling.
94174
fn default() -> Self {
95175
ManagerBuilder::new().build().unwrap()
96176
}
@@ -122,17 +202,22 @@ impl Connection {
122202
}
123203
}
124204

205+
/// A representation of a secret in Secrets Manager.
125206
#[derive(Debug, Clone)]
126207
pub struct Secret {
127208
query: String,
128209
connection: Arc<Connection>,
129210
}
130211

131212
impl Secret {
213+
/// Get the plaintext value of this secret.
214+
///
215+
/// Usually, this is in json format, but it can be any data format that you provide to Secrets Manager.
132216
pub async fn get_raw(&self) -> Result<String> {
133217
self.connection.get_secret(&self.query).await
134218
}
135219

220+
/// Get a value by name from within this secret.
136221
pub async fn get_single(&self, name: impl AsRef<str>) -> Result<String> {
137222
let raw = &self.get_raw().await?;
138223
let name = name.as_ref();
@@ -147,6 +232,7 @@ impl Secret {
147232
Ok(String::from(secret))
148233
}
149234

235+
/// Get the value of this secret, represented as a strongly-typed T.
150236
pub async fn get_typed<T: DeserializeOwned>(&self) -> Result<T> {
151237
let raw = self.get_raw().await?;
152238
Ok(serde_json::from_str(&raw)?)
@@ -167,28 +253,37 @@ struct ExtensionResponse {
167253
secret_string: String,
168254
}
169255

256+
/// A query for a specific [`Secret`] in AWS Secrets Manager. See [`Manager::get_secret`] for usage.
257+
///
258+
/// # Sealed
259+
/// You cannot implement this trait yourself.
170260
#[sealed]
171261
pub trait Query {
262+
#[doc(hidden)]
172263
fn get_query_string(&self) -> String;
173264
}
174265

266+
/// Flexible builder for a complex [`Query`].
175267
#[must_use = "continue building a query with the `with_version_id` or `with_version_stage` method"]
176268
pub struct QueryBuilder<'a> {
177269
secret_id: &'a str,
178270
}
179271

180272
impl<'a> QueryBuilder<'a> {
273+
/// Create a new builder with the secret name or ARN.
181274
pub fn new(secret_id: &'a str) -> Self {
182275
Self { secret_id }
183276
}
184277

278+
/// Create a query with a version id.
185279
pub fn with_version_id(self, version_id: &'a str) -> VersionIdQuery<'a> {
186280
VersionIdQuery {
187281
secret_id: self.secret_id,
188282
version_id,
189283
}
190284
}
191285

286+
/// Create a query with a version stage.
192287
pub fn with_version_stage(self, version_stage: &'a str) -> VersionStageQuery<'a> {
193288
VersionStageQuery {
194289
secret_id: self.secret_id,
@@ -197,32 +292,79 @@ impl<'a> QueryBuilder<'a> {
197292
}
198293
}
199294

295+
/// Query by the secret name or ARN.
296+
///
297+
/// This returns the current value of the secret (stage = "AWSCURRENT") and is usually what you want to use.
298+
///
299+
/// Any string-like type can be used, including [`String`], [`&str`], and [`std::borrow::Cow<str>`].
300+
///
301+
/// ```rust
302+
/// # use aws_parameters_and_secrets_lambda::ManagerBuilder;
303+
/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || {
304+
/// # let manager = ManagerBuilder::new().build()?;
305+
/// let secret = manager.get_secret("secret-name");
306+
/// # Ok::<_, anyhow::Error>(())
307+
/// # });
308+
/// ```
200309
#[sealed]
201310
impl<T: AsRef<str>> Query for T {
202311
fn get_query_string(&self) -> String {
203312
format!("secretId={}", self.as_ref())
204313
}
205314
}
206315

316+
/// A query for a secret with a version id. Create one via [`QueryBuilder::with_version_id`].
317+
///
318+
/// The version id is a unique identifier returned by Secrets Manager when a secret is created or updated.
207319
#[derive(Debug, Clone)]
208320
pub struct VersionIdQuery<'a> {
209321
secret_id: &'a str,
210322
version_id: &'a str,
211323
}
212324

325+
/// Query by the version id of the secret as well as the secret name or ARN.
326+
///
327+
/// ```rust
328+
/// # use aws_parameters_and_secrets_lambda::ManagerBuilder;
329+
/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || {
330+
/// # let manager = ManagerBuilder::new().build()?;
331+
/// use aws_parameters_and_secrets_lambda::QueryBuilder;
332+
///
333+
/// let query = QueryBuilder::new("secret-name")
334+
/// .with_version_id("18b94218-543d-4d67-aec5-f8e6a41f7813");
335+
/// let secret = manager.get_secret(query);
336+
/// # Ok::<_, anyhow::Error>(())
337+
/// # });
213338
#[sealed]
214339
impl Query for VersionIdQuery<'_> {
215340
fn get_query_string(&self) -> String {
216341
format!("secretId={}&versionId={}", self.secret_id, self.version_id)
217342
}
218343
}
219344

345+
/// A query for a secret with a version stage. Create one via [`QueryBuilder::with_version_stage`].
346+
///
347+
/// The "AWSCURRENT" stage is the current value of the secret, while the "AWSPREVIOUS" stage is the last value of the "AWSCURRENT" stage.
348+
/// You can also use your own stages.
220349
#[derive(Debug, Clone)]
221350
pub struct VersionStageQuery<'a> {
222351
secret_id: &'a str,
223352
version_stage: &'a str,
224353
}
225354

355+
/// Query by the stage of the secret as well as the secret name or ARN.
356+
///
357+
/// ```rust
358+
/// # use aws_parameters_and_secrets_lambda::ManagerBuilder;
359+
/// # temp_env::with_var("AWS_SESSION_TOKEN", Some("xyz"), || {
360+
/// # let manager = ManagerBuilder::new().build()?;
361+
/// use aws_parameters_and_secrets_lambda::QueryBuilder;
362+
///
363+
/// let query = QueryBuilder::new("secret-name")
364+
/// .with_version_stage("AWSPREVIOUS");
365+
/// let secret = manager.get_secret(query);
366+
/// # Ok::<_, anyhow::Error>(())
367+
/// # });
226368
#[sealed]
227369
impl Query for VersionStageQuery<'_> {
228370
fn get_query_string(&self) -> String {

0 commit comments

Comments
 (0)