Skip to main content

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}