Skip to content

Commit c795fce

Browse files
feat(redis-cloud): add AWS PrivateLink connectivity support (#406)
- Add PrivateLinkHandler with 10 methods for standard and Active-Active - Add delete_with_body() method to CloudClient for DELETE with JSON body - Add comprehensive test suite with 13 tests using wiremock - Update OpenAPI spec to latest version from redis.io - Achieve 100% coverage of all documented Cloud API endpoints Implements 6 new endpoints: - /subscriptions/{id}/private-link (GET, POST) - /subscriptions/{id}/private-link/principals (POST, DELETE) - /subscriptions/{id}/private-link/endpoint-script (GET) - /subscriptions/{id}/regions/{id}/private-link (GET, POST) - /subscriptions/{id}/regions/{id}/private-link/principals (POST, DELETE) - /subscriptions/{id}/regions/{id}/private-link/endpoint-script (GET) All tests passing (177 total), clippy clean, formatted.
1 parent 2a13296 commit c795fce

File tree

5 files changed

+814
-2
lines changed

5 files changed

+814
-2
lines changed

crates/redis-cloud/src/client.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ impl CloudClient {
301301
}
302302
}
303303

304+
/// Execute DELETE request with JSON body (used by some endpoints like PrivateLink principals)
305+
pub async fn delete_with_body<T: serde::de::DeserializeOwned>(
306+
&self,
307+
path: &str,
308+
body: serde_json::Value,
309+
) -> Result<T> {
310+
let url = self.normalize_url(path);
311+
312+
let response = self
313+
.client
314+
.delete(&url)
315+
.header("x-api-key", &self.api_key)
316+
.header("x-api-secret-key", &self.api_secret)
317+
.json(&body)
318+
.send()
319+
.await?;
320+
321+
self.handle_response(response).await
322+
}
323+
304324
/// Handle HTTP response
305325
async fn handle_response<T: serde::de::DeserializeOwned>(
306326
&self,

crates/redis-cloud/src/connectivity/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@
22
//!
33
//! This module manages advanced networking features for Redis Cloud Pro subscriptions,
44
//! including VPC peering, AWS Transit Gateway attachments, GCP Private Service Connect,
5-
//! and other cloud-native networking integrations.
5+
//! AWS PrivateLink, and other cloud-native networking integrations.
66
//!
77
//! # Supported Connectivity Types
88
//!
99
//! - **VPC Peering**: Direct peering between Redis Cloud VPC and your VPC
1010
//! - **Transit Gateway**: AWS Transit Gateway attachments for hub-and-spoke topologies
1111
//! - **Private Service Connect**: GCP Private Service Connect for private endpoints
12+
//! - **PrivateLink**: AWS PrivateLink for secure private connectivity
1213
//!
1314
//! # Module Organization
1415
//!
15-
//! The connectivity features are split into three specialized modules:
16+
//! The connectivity features are split into four specialized modules:
1617
//! - `vpc_peering` - VPC peering operations for AWS, GCP, and Azure
1718
//! - `psc` - Google Cloud Private Service Connect endpoints
1819
//! - `transit_gateway` - AWS Transit Gateway attachments
20+
//! - `private_link` - AWS PrivateLink connectivity
1921
22+
pub mod private_link;
2023
pub mod psc;
2124
pub mod transit_gateway;
2225
pub mod vpc_peering;
2326

2427
// Re-export handlers for convenience
28+
pub use private_link::PrivateLinkHandler;
2529
pub use psc::PscHandler;
2630
pub use transit_gateway::TransitGatewayHandler;
2731
pub use vpc_peering::VpcPeeringHandler;
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//! AWS PrivateLink connectivity operations
2+
//!
3+
//! This module provides AWS PrivateLink connectivity functionality for Redis Cloud,
4+
//! enabling secure, private connections from AWS VPCs to Redis Cloud databases.
5+
//!
6+
//! # Overview
7+
//!
8+
//! AWS PrivateLink allows you to connect to Redis Cloud from your AWS VPC without
9+
//! traversing the public internet. This provides enhanced security and potentially
10+
//! lower latency.
11+
//!
12+
//! # Features
13+
//!
14+
//! - **PrivateLink Management**: Create and retrieve PrivateLink configurations
15+
//! - **Principal Management**: Control which AWS principals can access the service
16+
//! - **Endpoint Scripts**: Get scripts to create endpoints in your AWS account
17+
//! - **Active-Active Support**: PrivateLink for CRDB (Active-Active) databases
18+
//!
19+
//! # Example Usage
20+
//!
21+
//! ```no_run
22+
//! use redis_cloud::{CloudClient, PrivateLinkHandler};
23+
//! use serde_json::json;
24+
//!
25+
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
26+
//! let client = CloudClient::builder()
27+
//! .api_key("your-api-key")
28+
//! .api_secret("your-api-secret")
29+
//! .build()?;
30+
//!
31+
//! let handler = PrivateLinkHandler::new(client);
32+
//!
33+
//! // Create a PrivateLink
34+
//! let request = json!({
35+
//! "shareName": "my-redis-share",
36+
//! "principal": "123456789012",
37+
//! "type": "aws_account",
38+
//! "alias": "Production Account"
39+
//! });
40+
//! let result = handler.create(123, request).await?;
41+
//!
42+
//! // Get PrivateLink configuration
43+
//! let config = handler.get(123).await?;
44+
//! # Ok(())
45+
//! # }
46+
//! ```
47+
48+
use crate::{CloudClient, Result};
49+
use serde_json::Value;
50+
51+
/// AWS PrivateLink handler
52+
///
53+
/// Manages AWS PrivateLink connectivity for Redis Cloud subscriptions.
54+
pub struct PrivateLinkHandler {
55+
client: CloudClient,
56+
}
57+
58+
impl PrivateLinkHandler {
59+
/// Create a new PrivateLink handler
60+
pub fn new(client: CloudClient) -> Self {
61+
Self { client }
62+
}
63+
64+
/// Get PrivateLink configuration
65+
///
66+
/// Gets the AWS PrivateLink configuration for a subscription.
67+
///
68+
/// GET /subscriptions/{subscriptionId}/private-link
69+
///
70+
/// # Arguments
71+
///
72+
/// * `subscription_id` - The subscription ID
73+
///
74+
/// # Returns
75+
///
76+
/// Returns the PrivateLink configuration as JSON
77+
pub async fn get(&self, subscription_id: i32) -> Result<Value> {
78+
self.client
79+
.get(&format!("/subscriptions/{}/private-link", subscription_id))
80+
.await
81+
}
82+
83+
/// Create a PrivateLink
84+
///
85+
/// Creates a new AWS PrivateLink configuration for a subscription.
86+
///
87+
/// POST /subscriptions/{subscriptionId}/private-link
88+
///
89+
/// # Arguments
90+
///
91+
/// * `subscription_id` - The subscription ID
92+
/// * `request` - PrivateLink creation request (shareName, principal, type required)
93+
///
94+
/// # Returns
95+
///
96+
/// Returns a task response that can be tracked for completion
97+
pub async fn create(&self, subscription_id: i32, request: Value) -> Result<Value> {
98+
self.client
99+
.post(
100+
&format!("/subscriptions/{}/private-link", subscription_id),
101+
&request,
102+
)
103+
.await
104+
}
105+
106+
/// Add principals to PrivateLink
107+
///
108+
/// Adds AWS principals (accounts, IAM roles, etc.) that can access the PrivateLink.
109+
///
110+
/// POST /subscriptions/{subscriptionId}/private-link/principals
111+
///
112+
/// # Arguments
113+
///
114+
/// * `subscription_id` - The subscription ID
115+
/// * `request` - Principal to add (principal required, type/alias optional)
116+
///
117+
/// # Returns
118+
///
119+
/// Returns the updated principal configuration
120+
pub async fn add_principals(&self, subscription_id: i32, request: Value) -> Result<Value> {
121+
self.client
122+
.post(
123+
&format!("/subscriptions/{}/private-link/principals", subscription_id),
124+
&request,
125+
)
126+
.await
127+
}
128+
129+
/// Remove principals from PrivateLink
130+
///
131+
/// Removes AWS principals from the PrivateLink access list.
132+
///
133+
/// DELETE /subscriptions/{subscriptionId}/private-link/principals
134+
///
135+
/// # Arguments
136+
///
137+
/// * `subscription_id` - The subscription ID
138+
/// * `request` - Principal to remove (principal, type, alias)
139+
///
140+
/// # Returns
141+
///
142+
/// Returns confirmation of deletion
143+
pub async fn remove_principals(&self, subscription_id: i32, request: Value) -> Result<Value> {
144+
self.client
145+
.delete_with_body(
146+
&format!("/subscriptions/{}/private-link/principals", subscription_id),
147+
request,
148+
)
149+
.await
150+
}
151+
152+
/// Get endpoint creation script
153+
///
154+
/// Gets a script to create the VPC endpoint in your AWS account.
155+
///
156+
/// GET /subscriptions/{subscriptionId}/private-link/endpoint-script
157+
///
158+
/// # Arguments
159+
///
160+
/// * `subscription_id` - The subscription ID
161+
///
162+
/// # Returns
163+
///
164+
/// Returns the endpoint creation script
165+
pub async fn get_endpoint_script(&self, subscription_id: i32) -> Result<Value> {
166+
self.client
167+
.get(&format!(
168+
"/subscriptions/{}/private-link/endpoint-script",
169+
subscription_id
170+
))
171+
.await
172+
}
173+
174+
/// Get Active-Active PrivateLink configuration
175+
///
176+
/// Gets the AWS PrivateLink configuration for an Active-Active (CRDB) subscription region.
177+
///
178+
/// GET /subscriptions/{subscriptionId}/regions/{regionId}/private-link
179+
///
180+
/// # Arguments
181+
///
182+
/// * `subscription_id` - The subscription ID
183+
/// * `region_id` - The region ID
184+
///
185+
/// # Returns
186+
///
187+
/// Returns the PrivateLink configuration for the region
188+
pub async fn get_active_active(&self, subscription_id: i32, region_id: i32) -> Result<Value> {
189+
self.client
190+
.get(&format!(
191+
"/subscriptions/{}/regions/{}/private-link",
192+
subscription_id, region_id
193+
))
194+
.await
195+
}
196+
197+
/// Create Active-Active PrivateLink
198+
///
199+
/// Creates a new AWS PrivateLink for an Active-Active (CRDB) subscription region.
200+
///
201+
/// POST /subscriptions/{subscriptionId}/regions/{regionId}/private-link
202+
///
203+
/// # Arguments
204+
///
205+
/// * `subscription_id` - The subscription ID
206+
/// * `region_id` - The region ID
207+
/// * `request` - PrivateLink creation request
208+
///
209+
/// # Returns
210+
///
211+
/// Returns a task response
212+
pub async fn create_active_active(
213+
&self,
214+
subscription_id: i32,
215+
region_id: i32,
216+
request: Value,
217+
) -> Result<Value> {
218+
self.client
219+
.post(
220+
&format!(
221+
"/subscriptions/{}/regions/{}/private-link",
222+
subscription_id, region_id
223+
),
224+
&request,
225+
)
226+
.await
227+
}
228+
229+
/// Add principals to Active-Active PrivateLink
230+
///
231+
/// Adds AWS principals to an Active-Active PrivateLink.
232+
///
233+
/// POST /subscriptions/{subscriptionId}/regions/{regionId}/private-link/principals
234+
///
235+
/// # Arguments
236+
///
237+
/// * `subscription_id` - The subscription ID
238+
/// * `region_id` - The region ID
239+
/// * `request` - Principal to add
240+
///
241+
/// # Returns
242+
///
243+
/// Returns the updated configuration
244+
pub async fn add_principals_active_active(
245+
&self,
246+
subscription_id: i32,
247+
region_id: i32,
248+
request: Value,
249+
) -> Result<Value> {
250+
self.client
251+
.post(
252+
&format!(
253+
"/subscriptions/{}/regions/{}/private-link/principals",
254+
subscription_id, region_id
255+
),
256+
&request,
257+
)
258+
.await
259+
}
260+
261+
/// Remove principals from Active-Active PrivateLink
262+
///
263+
/// Removes AWS principals from an Active-Active PrivateLink.
264+
///
265+
/// DELETE /subscriptions/{subscriptionId}/regions/{regionId}/private-link/principals
266+
///
267+
/// # Arguments
268+
///
269+
/// * `subscription_id` - The subscription ID
270+
/// * `region_id` - The region ID
271+
/// * `request` - Principal to remove
272+
///
273+
/// # Returns
274+
///
275+
/// Returns confirmation of deletion
276+
pub async fn remove_principals_active_active(
277+
&self,
278+
subscription_id: i32,
279+
region_id: i32,
280+
request: Value,
281+
) -> Result<Value> {
282+
self.client
283+
.delete_with_body(
284+
&format!(
285+
"/subscriptions/{}/regions/{}/private-link/principals",
286+
subscription_id, region_id
287+
),
288+
request,
289+
)
290+
.await
291+
}
292+
293+
/// Get Active-Active endpoint creation script
294+
///
295+
/// Gets a script to create the VPC endpoint for an Active-Active region.
296+
///
297+
/// GET /subscriptions/{subscriptionId}/regions/{regionId}/private-link/endpoint-script
298+
///
299+
/// # Arguments
300+
///
301+
/// * `subscription_id` - The subscription ID
302+
/// * `region_id` - The region ID
303+
///
304+
/// # Returns
305+
///
306+
/// Returns the endpoint creation script
307+
pub async fn get_endpoint_script_active_active(
308+
&self,
309+
subscription_id: i32,
310+
region_id: i32,
311+
) -> Result<Value> {
312+
self.client
313+
.get(&format!(
314+
"/subscriptions/{}/regions/{}/private-link/endpoint-script",
315+
subscription_id, region_id
316+
))
317+
.await
318+
}
319+
}

crates/redis-cloud/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ pub use acl::AclHandler;
309309
pub use cloud_accounts::CloudAccountsHandler as CloudAccountHandler;
310310

311311
// Connectivity handlers
312+
pub use connectivity::private_link::PrivateLinkHandler;
312313
pub use connectivity::psc::PscHandler;
313314
pub use connectivity::transit_gateway::TransitGatewayHandler;
314315
pub use connectivity::vpc_peering::VpcPeeringHandler;

0 commit comments

Comments
 (0)