redis_cloud/connectivity/private_link.rs
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, PrivateLinkCreateRequest, PrincipalType};
23//!
24//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
25//! let client = CloudClient::builder()
26//! .api_key("your-api-key")
27//! .api_secret("your-api-secret")
28//! .build()?;
29//!
30//! let handler = PrivateLinkHandler::new(client);
31//!
32//! // Create a PrivateLink
33//! let request = PrivateLinkCreateRequest {
34//! share_name: "my-redis-share".to_string(),
35//! principal: "123456789012".to_string(),
36//! principal_type: PrincipalType::AwsAccount,
37//! alias: Some("Production Account".to_string()),
38//! };
39//! let result = handler.create(123, &request).await?;
40//!
41//! // Get PrivateLink configuration
42//! let config = handler.get(123).await?;
43//! # Ok(())
44//! # }
45//! ```
46
47use crate::types::TaskStateUpdate;
48use crate::{CloudClient, Result};
49use serde::{Deserialize, Serialize};
50
51// ============================================================================
52// Request/Response Types
53// ============================================================================
54
55/// Principal type for `PrivateLink` access control
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum PrincipalType {
59 /// AWS account ID
60 AwsAccount,
61 /// AWS Organization
62 Organization,
63 /// AWS Organization Unit
64 OrganizationUnit,
65 /// AWS IAM Role
66 IamRole,
67 /// AWS IAM User
68 IamUser,
69 /// Service Principal
70 ServicePrincipal,
71}
72
73/// Request to create a `PrivateLink` configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct PrivateLinkCreateRequest {
77 /// Share name for the `PrivateLink` service (max 64 characters)
78 pub share_name: String,
79
80 /// AWS principal (account ID, role ARN, etc.)
81 pub principal: String,
82
83 /// Type of principal
84 #[serde(rename = "type")]
85 pub principal_type: PrincipalType,
86
87 /// Optional alias for the `PrivateLink`
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub alias: Option<String>,
90}
91
92/// Request to add a principal to `PrivateLink` access list
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct PrivateLinkAddPrincipalRequest {
96 /// AWS principal (account ID, role ARN, etc.)
97 pub principal: String,
98
99 /// Type of principal
100 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
101 pub principal_type: Option<PrincipalType>,
102
103 /// Optional alias for the principal
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub alias: Option<String>,
106}
107
108/// Request to remove a principal from `PrivateLink` access list
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct PrivateLinkRemovePrincipalRequest {
112 /// AWS principal to remove
113 pub principal: String,
114
115 /// Type of principal
116 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
117 pub principal_type: Option<PrincipalType>,
118
119 /// Alias of the principal
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub alias: Option<String>,
122}
123
124/// A single `PrivateLink` connection to disassociate.
125///
126/// Matches the `PrivateLinkConnectionDisassociate` schema. `associationId`,
127/// `type`, and `principalId` are required by the spec.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct PrivateLinkConnectionDisassociate {
131 /// Resource share association ID.
132 pub association_id: String,
133
134 /// VPC endpoint connection ID.
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub connection_id: Option<String>,
137
138 /// Type of the connection.
139 #[serde(rename = "type")]
140 pub connection_type: String,
141
142 /// Principal ID that owns the connection, e.g. AWS account ID.
143 pub principal_id: String,
144}
145
146/// Request to disassociate connections from a `PrivateLink`.
147///
148/// Matches the `PrivateLinkConnectionsDisassociateRequest` schema.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct PrivateLinkConnectionsDisassociateRequest {
152 /// Subscription that owns the `PrivateLink`. Server-populated from the path.
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub subscription_id: Option<i32>,
155
156 /// Connections to disassociate from the `PrivateLink`.
157 pub connections: Vec<PrivateLinkConnectionDisassociate>,
158
159 /// Read-only on the response; populated by the server with the operation
160 /// type.
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub command_type: Option<String>,
163}
164
165/// Request to disassociate connections from an Active-Active `PrivateLink`.
166///
167/// Matches the `PrivateLinkActiveActiveConnectionsDisassociateRequest` schema.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct PrivateLinkActiveActiveConnectionsDisassociateRequest {
171 /// Subscription that owns the `PrivateLink`. Server-populated from the path.
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub subscription_id: Option<i32>,
174
175 /// Deployment region ID as defined by the cloud provider. Server-populated
176 /// from the path.
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub region_id: Option<i32>,
179
180 /// Connections to disassociate from the `PrivateLink`.
181 pub connections: Vec<PrivateLinkConnectionDisassociate>,
182
183 /// Read-only on the response; populated by the server with the operation
184 /// type.
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub command_type: Option<String>,
187}
188
189/// `PrivateLink` configuration response
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct PrivateLink {
193 /// `PrivateLink` status
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub status: Option<String>,
196
197 /// List of principals with access
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub principals: Option<Vec<PrivateLinkPrincipal>>,
200
201 /// AWS Resource Configuration ID
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub resource_configuration_id: Option<String>,
204
205 /// AWS Resource Configuration ARN
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub resource_configuration_arn: Option<String>,
208
209 /// RAM share ARN
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub share_arn: Option<String>,
212
213 /// Share name
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub share_name: Option<String>,
216
217 /// List of `PrivateLink` connections
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub connections: Option<Vec<PrivateLinkConnection>>,
220
221 /// List of databases accessible via `PrivateLink`
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub databases: Option<Vec<PrivateLinkDatabase>>,
224
225 /// Subscription ID
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub subscription_id: Option<i32>,
228
229 /// Region ID (for Active-Active)
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub region_id: Option<i32>,
232
233 /// Error message if any
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub error_message: Option<String>,
236}
237
238/// `PrivateLink` principal information
239#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct PrivateLinkPrincipal {
242 /// AWS principal (account ID, role ARN, etc.)
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub principal: Option<String>,
245
246 /// Type of principal
247 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
248 pub principal_type: Option<String>,
249
250 /// Alias for the principal
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub alias: Option<String>,
253
254 /// Principal status
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub status: Option<String>,
257}
258
259/// `PrivateLink` connection information
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct PrivateLinkConnection {
263 /// Association ID
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub association_id: Option<String>,
266
267 /// Connection ID
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub connection_id: Option<String>,
270
271 /// Connection type
272 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
273 pub connection_type: Option<String>,
274
275 /// Owner ID (AWS account)
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub owner_id: Option<String>,
278
279 /// Association date
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub association_date: Option<String>,
282}
283
284/// Database accessible via `PrivateLink`
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct PrivateLinkDatabase {
288 /// Database ID
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub database_id: Option<i32>,
291
292 /// Database port
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub port: Option<i32>,
295
296 /// Resource link endpoint URL
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub resource_link_endpoint: Option<String>,
299}
300
301/// `PrivateLink` endpoint script response
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct PrivateLinkEndpointScript {
305 /// AWS CLI/CloudFormation script
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub resource_endpoint_script: Option<String>,
308
309 /// Terraform AWS script
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub terraform_aws_script: Option<String>,
312}
313
314/// AWS `PrivateLink` handler
315///
316/// Manages AWS `PrivateLink` connectivity for Redis Cloud subscriptions.
317pub struct PrivateLinkHandler {
318 client: CloudClient,
319}
320
321impl PrivateLinkHandler {
322 /// Create a new `PrivateLink` handler
323 #[must_use]
324 pub fn new(client: CloudClient) -> Self {
325 Self { client }
326 }
327
328 /// Get `PrivateLink` configuration
329 ///
330 /// Gets the AWS `PrivateLink` configuration for a subscription.
331 ///
332 /// GET /subscriptions/{subscriptionId}/private-link
333 ///
334 /// # Arguments
335 ///
336 /// * `subscription_id` - The subscription ID
337 ///
338 /// # Returns
339 ///
340 /// Returns a [`TaskStateUpdate`] to poll; the configuration is available
341 /// once the task completes.
342 pub async fn get(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
343 self.client
344 .get(&format!("/subscriptions/{subscription_id}/private-link"))
345 .await
346 }
347
348 /// Create a `PrivateLink`
349 ///
350 /// Creates a new AWS `PrivateLink` configuration for a subscription.
351 ///
352 /// POST /subscriptions/{subscriptionId}/private-link
353 ///
354 /// # Arguments
355 ///
356 /// * `subscription_id` - The subscription ID
357 /// * `request` - `PrivateLink` creation request
358 ///
359 /// # Returns
360 ///
361 /// Returns a task response that can be tracked for completion
362 pub async fn create(
363 &self,
364 subscription_id: i32,
365 request: &PrivateLinkCreateRequest,
366 ) -> Result<TaskStateUpdate> {
367 self.client
368 .post(
369 &format!("/subscriptions/{subscription_id}/private-link"),
370 request,
371 )
372 .await
373 }
374
375 /// Add principals to `PrivateLink`
376 ///
377 /// Adds AWS principals (accounts, IAM roles, etc.) that can access the `PrivateLink`.
378 ///
379 /// POST /subscriptions/{subscriptionId}/private-link/principals
380 ///
381 /// # Arguments
382 ///
383 /// * `subscription_id` - The subscription ID
384 /// * `request` - Principal to add
385 ///
386 /// # Returns
387 ///
388 /// Returns a [`TaskStateUpdate`] to poll for completion.
389 pub async fn add_principals(
390 &self,
391 subscription_id: i32,
392 request: &PrivateLinkAddPrincipalRequest,
393 ) -> Result<TaskStateUpdate> {
394 self.client
395 .post(
396 &format!("/subscriptions/{subscription_id}/private-link/principals"),
397 request,
398 )
399 .await
400 }
401
402 /// Remove principals from `PrivateLink`
403 ///
404 /// Removes AWS principals from the `PrivateLink` access list.
405 ///
406 /// DELETE /subscriptions/{subscriptionId}/private-link/principals
407 ///
408 /// # Arguments
409 ///
410 /// * `subscription_id` - The subscription ID
411 /// * `request` - Principal to remove
412 ///
413 /// # Returns
414 ///
415 /// Returns a [`TaskStateUpdate`] to poll the asynchronous deletion.
416 pub async fn remove_principals(
417 &self,
418 subscription_id: i32,
419 request: &PrivateLinkRemovePrincipalRequest,
420 ) -> Result<TaskStateUpdate> {
421 self.client
422 .delete_with_body(
423 &format!("/subscriptions/{subscription_id}/private-link/principals"),
424 serde_json::to_value(request).unwrap_or_default(),
425 )
426 .await
427 }
428
429 /// Get endpoint creation script
430 ///
431 /// Gets a script to create the VPC endpoint in your AWS account.
432 ///
433 /// GET /subscriptions/{subscriptionId}/private-link/endpoint-script
434 ///
435 /// # Arguments
436 ///
437 /// * `subscription_id` - The subscription ID
438 ///
439 /// # Returns
440 ///
441 /// Returns a [`TaskStateUpdate`]; the generated script is available once
442 /// the task completes.
443 pub async fn get_endpoint_script(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
444 self.client
445 .get(&format!(
446 "/subscriptions/{subscription_id}/private-link/endpoint-script"
447 ))
448 .await
449 }
450
451 /// Delete `PrivateLink`
452 ///
453 /// Deletes the AWS `PrivateLink` configuration for a subscription.
454 ///
455 /// DELETE /subscriptions/{subscriptionId}/private-link
456 ///
457 /// # Arguments
458 ///
459 /// * `subscription_id` - The subscription ID
460 ///
461 /// # Returns
462 ///
463 /// Returns task information for tracking the deletion
464 pub async fn delete(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
465 self.client
466 .delete_typed(&format!("/subscriptions/{subscription_id}/private-link"))
467 .await
468 }
469
470 /// Get Active-Active `PrivateLink` configuration
471 ///
472 /// Gets the AWS `PrivateLink` configuration for an Active-Active (CRDB) subscription region.
473 ///
474 /// GET /subscriptions/{subscriptionId}/regions/{regionId}/private-link
475 ///
476 /// # Arguments
477 ///
478 /// * `subscription_id` - The subscription ID
479 /// * `region_id` - The region ID
480 ///
481 /// # Returns
482 ///
483 /// Returns a [`TaskStateUpdate`] to poll; the configuration is available
484 /// once the task completes.
485 pub async fn get_active_active(
486 &self,
487 subscription_id: i32,
488 region_id: i32,
489 ) -> Result<TaskStateUpdate> {
490 self.client
491 .get(&format!(
492 "/subscriptions/{subscription_id}/regions/{region_id}/private-link"
493 ))
494 .await
495 }
496
497 /// Create Active-Active `PrivateLink`
498 ///
499 /// Creates a new AWS `PrivateLink` for an Active-Active (CRDB) subscription region.
500 ///
501 /// POST /subscriptions/{subscriptionId}/regions/{regionId}/private-link
502 ///
503 /// # Arguments
504 ///
505 /// * `subscription_id` - The subscription ID
506 /// * `region_id` - The region ID
507 /// * `request` - `PrivateLink` creation request
508 ///
509 /// # Returns
510 ///
511 /// Returns a task response
512 pub async fn create_active_active(
513 &self,
514 subscription_id: i32,
515 region_id: i32,
516 request: &PrivateLinkCreateRequest,
517 ) -> Result<TaskStateUpdate> {
518 self.client
519 .post(
520 &format!("/subscriptions/{subscription_id}/regions/{region_id}/private-link"),
521 request,
522 )
523 .await
524 }
525
526 /// Add principals to Active-Active `PrivateLink`
527 ///
528 /// Adds AWS principals to an Active-Active `PrivateLink`.
529 ///
530 /// POST /subscriptions/{subscriptionId}/regions/{regionId}/private-link/principals
531 ///
532 /// # Arguments
533 ///
534 /// * `subscription_id` - The subscription ID
535 /// * `region_id` - The region ID
536 /// * `request` - Principal to add
537 ///
538 /// # Returns
539 ///
540 /// Returns a [`TaskStateUpdate`] to poll for completion.
541 pub async fn add_principals_active_active(
542 &self,
543 subscription_id: i32,
544 region_id: i32,
545 request: &PrivateLinkAddPrincipalRequest,
546 ) -> Result<TaskStateUpdate> {
547 self.client
548 .post(
549 &format!(
550 "/subscriptions/{subscription_id}/regions/{region_id}/private-link/principals"
551 ),
552 request,
553 )
554 .await
555 }
556
557 /// Remove principals from Active-Active `PrivateLink`
558 ///
559 /// Removes AWS principals from an Active-Active `PrivateLink`.
560 ///
561 /// DELETE /subscriptions/{subscriptionId}/regions/{regionId}/private-link/principals
562 ///
563 /// # Arguments
564 ///
565 /// * `subscription_id` - The subscription ID
566 /// * `region_id` - The region ID
567 /// * `request` - Principal to remove
568 ///
569 /// # Returns
570 ///
571 /// Returns a [`TaskStateUpdate`] to poll the asynchronous deletion.
572 pub async fn remove_principals_active_active(
573 &self,
574 subscription_id: i32,
575 region_id: i32,
576 request: &PrivateLinkRemovePrincipalRequest,
577 ) -> Result<TaskStateUpdate> {
578 self.client
579 .delete_with_body(
580 &format!(
581 "/subscriptions/{subscription_id}/regions/{region_id}/private-link/principals"
582 ),
583 serde_json::to_value(request).unwrap_or_default(),
584 )
585 .await
586 }
587
588 /// Get Active-Active endpoint creation script
589 ///
590 /// Gets a script to create the VPC endpoint for an Active-Active region.
591 ///
592 /// GET /subscriptions/{subscriptionId}/regions/{regionId}/private-link/endpoint-script
593 ///
594 /// # Arguments
595 ///
596 /// * `subscription_id` - The subscription ID
597 /// * `region_id` - The region ID
598 ///
599 /// # Returns
600 ///
601 /// Returns a [`TaskStateUpdate`]; the generated script is available once
602 /// the task completes.
603 pub async fn get_endpoint_script_active_active(
604 &self,
605 subscription_id: i32,
606 region_id: i32,
607 ) -> Result<TaskStateUpdate> {
608 self.client
609 .get(&format!(
610 "/subscriptions/{subscription_id}/regions/{region_id}/private-link/endpoint-script"
611 ))
612 .await
613 }
614
615 /// Disassociate connections from a `PrivateLink`
616 ///
617 /// Disassociates one or more VPC endpoint connections from the AWS
618 /// `PrivateLink` configuration for a subscription.
619 ///
620 /// POST /subscriptions/{subscriptionId}/private-link/connections/disassociate
621 ///
622 /// # Arguments
623 ///
624 /// * `subscription_id` - The subscription ID
625 /// * `request` - The connections to disassociate
626 ///
627 /// # Returns
628 ///
629 /// Returns a [`TaskStateUpdate`] to poll for completion.
630 pub async fn disassociate_connections(
631 &self,
632 subscription_id: i32,
633 request: &PrivateLinkConnectionsDisassociateRequest,
634 ) -> Result<TaskStateUpdate> {
635 self.client
636 .post(
637 &format!("/subscriptions/{subscription_id}/private-link/connections/disassociate"),
638 request,
639 )
640 .await
641 }
642
643 /// Delete Active-Active `PrivateLink`
644 ///
645 /// Deletes the AWS `PrivateLink` configuration for an Active-Active (CRDB)
646 /// subscription region.
647 ///
648 /// DELETE /subscriptions/{subscriptionId}/regions/{regionId}/private-link
649 ///
650 /// # Arguments
651 ///
652 /// * `subscription_id` - The subscription ID
653 /// * `region_id` - The region ID
654 ///
655 /// # Returns
656 ///
657 /// Returns a [`TaskStateUpdate`] to poll the asynchronous deletion.
658 pub async fn delete_active_active(
659 &self,
660 subscription_id: i32,
661 region_id: i32,
662 ) -> Result<TaskStateUpdate> {
663 self.client
664 .delete_typed(&format!(
665 "/subscriptions/{subscription_id}/regions/{region_id}/private-link"
666 ))
667 .await
668 }
669
670 /// Disassociate connections from an Active-Active `PrivateLink`
671 ///
672 /// Disassociates one or more VPC endpoint connections from the AWS
673 /// `PrivateLink` configuration for an Active-Active subscription region.
674 ///
675 /// POST /subscriptions/{subscriptionId}/regions/{regionId}/private-link/connections/disassociate
676 ///
677 /// # Arguments
678 ///
679 /// * `subscription_id` - The subscription ID
680 /// * `region_id` - The region ID
681 /// * `request` - The connections to disassociate
682 ///
683 /// # Returns
684 ///
685 /// Returns a [`TaskStateUpdate`] to poll for completion.
686 pub async fn disassociate_connections_active_active(
687 &self,
688 subscription_id: i32,
689 region_id: i32,
690 request: &PrivateLinkActiveActiveConnectionsDisassociateRequest,
691 ) -> Result<TaskStateUpdate> {
692 self.client
693 .post(
694 &format!(
695 "/subscriptions/{subscription_id}/regions/{region_id}/private-link/connections/disassociate"
696 ),
697 request,
698 )
699 .await
700 }
701}