Skip to main content

redis_cloud/connectivity/
vpc_peering.rs

1//! VPC peering operations for AWS and GCP Pro subscriptions.
2//!
3//! Manages VPC peering connections between a Redis Cloud subscription's
4//! VPC and a customer-owned VPC, covering both standard subscriptions and
5//! Active-Active (CRDB) subscriptions where each region is peered
6//! independently.
7//!
8//! # When to use this module
9//!
10//! - You want direct VPC-to-VPC private connectivity (no public
11//!   endpoint, no shared TGW).
12//! - The subscription is on **AWS** or **GCP**. Azure connectivity is
13//!   handled separately by the Redis Cloud console; the SDK does not
14//!   yet expose Azure-specific endpoints here.
15//!
16//! For AWS hub-and-spoke topologies see
17//! [`crate::connectivity::transit_gateway`]; for AWS endpoint-style
18//! private connectivity see [`crate::connectivity::private_link`]; for
19//! GCP endpoint-style private connectivity see
20//! [`crate::connectivity::psc`].
21//!
22//! # Endpoint surface
23//!
24//! - `GET    /subscriptions/{subscriptionId}/peerings`
25//! - `POST   /subscriptions/{subscriptionId}/peerings`
26//! - `PUT    /subscriptions/{subscriptionId}/peerings/{peeringId}`
27//! - `DELETE /subscriptions/{subscriptionId}/peerings/{peeringId}`
28//!
29//! Active-Active subscriptions peer each region independently under
30//! `/subscriptions/{subscriptionId}/regions/peerings[/{peeringId}]`.
31//!
32//! # Example
33//!
34//! Construct a provider-targeted body and create a peering:
35//!
36//! ```rust,no_run
37//! use redis_cloud::{CloudClient, VpcPeeringHandler};
38//! use redis_cloud::connectivity::VpcPeeringCreateRequest;
39//!
40//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
41//! let client = CloudClient::builder()
42//!     .api_key("k").api_secret("s").build()?;
43//! let handler = VpcPeeringHandler::new(client);
44//!
45//! let mut request = VpcPeeringCreateRequest::for_aws(
46//!     "us-east-1", "123456789012", "vpc-12345678",
47//! );
48//! request.vpc_cidr = Some("10.0.0.0/16".to_string());
49//! let task = handler.create(123, &request).await?;
50//! # let _ = task;
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! # Errors
56//!
57//! All operations return [`crate::Result`]; transport, auth, and 4xx/5xx
58//! responses surface as the corresponding [`crate::CloudError`] variant.
59
60use crate::{CloudClient, Result};
61use serde::{Deserialize, Serialize};
62
63/// VPC peering creation request.
64///
65/// The Redis Cloud API documents this as a `oneOf` between an AWS-shaped
66/// body (requiring `region`, `awsAccountId`, `vpcId`) and a GCP-shaped body
67/// (requiring `vpcProjectUid`, `vpcNetworkName`). This struct keeps both
68/// providers in one type for caller flexibility, but uses
69/// `#[serde(rename = ...)]` so the AWS and GCP fields serialize to the
70/// **exact wire names the spec requires**. Use [`Self::for_aws`] or
71/// [`Self::for_gcp`] to construct provider-targeted bodies that avoid
72/// mixing fields.
73///
74/// A type-safe enum split that prevents AWS+GCP field mixing at compile
75/// time is tracked as a follow-on under #65.
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct VpcPeeringCreateRequest {
79    /// Cloud provider discriminator (e.g. "AWS", "GCP").
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub provider: Option<String>,
82
83    /// Read-only on the response; populated by the server.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub command_type: Option<String>,
86
87    // ------- AWS body -------
88    /// AWS region. Wire name: `region` (spec required for AWS).
89    #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
90    pub aws_region: Option<String>,
91
92    /// AWS account ID (spec required for AWS).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub aws_account_id: Option<String>,
95
96    /// AWS VPC ID (spec required for AWS).
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub vpc_id: Option<String>,
99
100    /// VPC CIDR. AWS only; optional.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub vpc_cidr: Option<String>,
103
104    /// List of VPC CIDRs. AWS only; optional.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub vpc_cidrs: Option<Vec<String>>,
107
108    // ------- GCP body -------
109    /// GCP project UID. Wire name: `vpcProjectUid` (spec required for GCP).
110    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
111    pub gcp_project_id: Option<String>,
112
113    /// GCP network name. Wire name: `vpcNetworkName` (spec required for GCP).
114    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
115    pub network_name: Option<String>,
116}
117
118impl VpcPeeringCreateRequest {
119    /// Construct an AWS-targeted VPC peering creation body.
120    ///
121    /// Pre-populates `provider = "AWS"` and the three required AWS fields
122    /// (`region`, `awsAccountId`, `vpcId`). Optional CIDR fields can be set
123    /// directly on the returned struct.
124    #[must_use]
125    pub fn for_aws(
126        region: impl Into<String>,
127        aws_account_id: impl Into<String>,
128        vpc_id: impl Into<String>,
129    ) -> Self {
130        Self {
131            provider: Some("AWS".to_string()),
132            aws_region: Some(region.into()),
133            aws_account_id: Some(aws_account_id.into()),
134            vpc_id: Some(vpc_id.into()),
135            ..Self::default()
136        }
137    }
138
139    /// Construct a GCP-targeted VPC peering creation body.
140    ///
141    /// Pre-populates `provider = "GCP"` and the two required GCP fields
142    /// (`vpcProjectUid`, `vpcNetworkName`).
143    #[must_use]
144    pub fn for_gcp(project_uid: impl Into<String>, network_name: impl Into<String>) -> Self {
145        Self {
146            provider: Some("GCP".to_string()),
147            gcp_project_id: Some(project_uid.into()),
148            network_name: Some(network_name.into()),
149            ..Self::default()
150        }
151    }
152}
153
154/// Base VPC peering creation request (for backward compatibility)
155pub type VpcPeeringCreateBaseRequest = VpcPeeringCreateRequest;
156
157/// Active-Active VPC peering creation request.
158///
159/// The Redis Cloud API documents this as a `oneOf` between an AWS-shaped body
160/// (requiring `sourceRegion`, `destinationRegion`, `awsAccountId`, `vpcId`) and
161/// a GCP-shaped body (requiring `sourceRegion`, `vpcProjectUid`,
162/// `vpcNetworkName`). Like [`VpcPeeringCreateRequest`], both providers live in
163/// one struct and use `#[serde(rename = ...)]` so each field serializes to the
164/// **exact wire name the spec requires**. Use [`Self::for_aws`] or
165/// [`Self::for_gcp`] to construct provider-targeted bodies that avoid mixing
166/// fields.
167///
168/// A type-safe enum split that prevents AWS+GCP field mixing at compile time is
169/// tracked as a follow-on under #65.
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct ActiveActiveVpcPeeringCreateRequest {
173    /// Cloud provider discriminator (e.g. "AWS", "GCP").
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub provider: Option<String>,
176
177    /// Read-only on the response; populated by the server.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub command_type: Option<String>,
180
181    /// Name of the region to create the VPC peering from. Required for both
182    /// providers.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub source_region: Option<String>,
185
186    // ------- AWS body -------
187    /// Name of the region to create the VPC peering to. AWS only; required.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub destination_region: Option<String>,
190
191    /// AWS account ID (spec required for AWS).
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub aws_account_id: Option<String>,
194
195    /// AWS VPC ID (spec required for AWS).
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub vpc_id: Option<String>,
198
199    /// VPC CIDR. AWS only; optional.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub vpc_cidr: Option<String>,
202
203    /// List of VPC CIDRs. AWS only; optional.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub vpc_cidrs: Option<Vec<String>>,
206
207    // ------- GCP body -------
208    /// GCP project UID. Wire name: `vpcProjectUid` (spec required for GCP).
209    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
210    pub gcp_project_id: Option<String>,
211
212    /// GCP network name. Wire name: `vpcNetworkName` (spec required for GCP).
213    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
214    pub network_name: Option<String>,
215}
216
217impl ActiveActiveVpcPeeringCreateRequest {
218    /// Construct an AWS-targeted Active-Active VPC peering creation body.
219    ///
220    /// Pre-populates `provider = "AWS"` and the four required AWS fields
221    /// (`sourceRegion`, `destinationRegion`, `awsAccountId`, `vpcId`). Optional
222    /// CIDR fields can be set directly on the returned struct.
223    #[must_use]
224    pub fn for_aws(
225        source_region: impl Into<String>,
226        destination_region: impl Into<String>,
227        aws_account_id: impl Into<String>,
228        vpc_id: impl Into<String>,
229    ) -> Self {
230        Self {
231            provider: Some("AWS".to_string()),
232            source_region: Some(source_region.into()),
233            destination_region: Some(destination_region.into()),
234            aws_account_id: Some(aws_account_id.into()),
235            vpc_id: Some(vpc_id.into()),
236            ..Self::default()
237        }
238    }
239
240    /// Construct a GCP-targeted Active-Active VPC peering creation body.
241    ///
242    /// Pre-populates `provider = "GCP"` and the three required GCP fields
243    /// (`sourceRegion`, `vpcProjectUid`, `vpcNetworkName`).
244    #[must_use]
245    pub fn for_gcp(
246        source_region: impl Into<String>,
247        project_uid: impl Into<String>,
248        network_name: impl Into<String>,
249    ) -> Self {
250        Self {
251            provider: Some("GCP".to_string()),
252            source_region: Some(source_region.into()),
253            gcp_project_id: Some(project_uid.into()),
254            network_name: Some(network_name.into()),
255            ..Self::default()
256        }
257    }
258}
259
260/// VPC peering update request for AWS
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct VpcPeeringUpdateAwsRequest {
264    /// Subscription that owns the peering. Server-populated from the path.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub subscription_id: Option<i32>,
267
268    /// VPC Peering ID to update.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub vpc_peering_id: Option<i32>,
271
272    /// Optional. VPC CIDR.
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub vpc_cidr: Option<String>,
275
276    /// Optional. List of VPC CIDRs.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub vpc_cidrs: Option<Vec<String>>,
279
280    /// Read-only on the response; populated by the server with the
281    /// operation type (e.g. `"UPDATE_VPC_PEERING"`).
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub command_type: Option<String>,
284}
285
286/// VPC peering update request (generic)
287pub type VpcPeeringUpdateRequest = VpcPeeringUpdateAwsRequest;
288
289/// Task state update response
290pub use crate::types::TaskStateUpdate;
291
292/// VPC CIDR with status
293#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct VpcCidr {
296    /// VPC CIDR block
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub vpc_cidr: Option<String>,
299
300    /// CIDR status (active/inactive)
301    #[serde(rename = "active", skip_serializing_if = "Option::is_none")]
302    pub status: Option<String>,
303}
304
305/// VPC Peering information
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct VpcPeering {
309    /// VPC Peering ID
310    #[serde(rename = "vpcPeeringId", skip_serializing_if = "Option::is_none")]
311    pub id: Option<i32>,
312
313    /// Peering status (e.g., "active", "pending-acceptance")
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub status: Option<String>,
316
317    /// AWS account ID
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub aws_account_id: Option<String>,
320
321    /// AWS VPC peering connection ID
322    #[serde(rename = "awsPeeringUid", skip_serializing_if = "Option::is_none")]
323    pub aws_peering_id: Option<String>,
324
325    /// VPC ID
326    #[serde(rename = "vpcUid", skip_serializing_if = "Option::is_none")]
327    pub vpc_id: Option<String>,
328
329    /// VPC CIDR
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub vpc_cidr: Option<String>,
332
333    /// List of VPC CIDRs with status
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub vpc_cidrs: Option<Vec<VpcCidr>>,
336
337    /// GCP project UID
338    #[serde(rename = "projectUid", skip_serializing_if = "Option::is_none")]
339    pub gcp_project_uid: Option<String>,
340
341    /// GCP network name
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub network_name: Option<String>,
344
345    /// Redis GCP project UID
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub redis_project_uid: Option<String>,
348
349    /// Redis GCP network name
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub redis_network_name: Option<String>,
352
353    /// Cloud peering ID (GCP)
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub cloud_peering_id: Option<String>,
356
357    /// Cloud provider region
358    #[serde(rename = "regionName", skip_serializing_if = "Option::is_none")]
359    pub region: Option<String>,
360
361    /// Cloud provider (AWS, GCP, Azure)
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub provider: Option<String>,
364}
365
366/// Active-Active VPC Peering information
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct ActiveActiveVpcPeering {
370    /// VPC Peering ID
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub id: Option<i32>,
373
374    /// Peering status
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub status: Option<String>,
377
378    /// Region ID
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub region_id: Option<i32>,
381
382    /// Region name
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub region_name: Option<String>,
385
386    /// AWS account ID
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub aws_account_id: Option<String>,
389
390    /// AWS VPC peering UID
391    #[serde(rename = "awsPeeringUid", skip_serializing_if = "Option::is_none")]
392    pub aws_peering_id: Option<String>,
393
394    /// VPC UID
395    #[serde(rename = "vpcUid", skip_serializing_if = "Option::is_none")]
396    pub vpc_id: Option<String>,
397
398    /// VPC CIDR
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub vpc_cidr: Option<String>,
401
402    /// List of VPC CIDRs with status
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub vpc_cidrs: Option<Vec<VpcCidr>>,
405
406    /// GCP project UID
407    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
408    pub gcp_project_uid: Option<String>,
409
410    /// GCP network name
411    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
412    pub network_name: Option<String>,
413
414    /// Redis GCP project UID
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub redis_project_uid: Option<String>,
417
418    /// Redis GCP network name
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub redis_network_name: Option<String>,
421
422    /// Cloud peering ID
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub cloud_peering_id: Option<String>,
425
426    /// Source region
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub source_region: Option<String>,
429
430    /// Destination region
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub destination_region: Option<String>,
433}
434
435/// Active-Active VPC Peering region
436#[derive(Debug, Clone, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct ActiveActiveVpcRegion {
439    /// Region ID
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub id: Option<i32>,
442
443    /// Source region name
444    #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
445    pub source_region: Option<String>,
446
447    /// VPC Peerings in this region
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub vpc_peerings: Option<Vec<ActiveActiveVpcPeering>>,
450}
451
452/// Active-Active VPC Peering list response
453#[derive(Debug, Clone, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct ActiveActiveVpcPeeringList {
456    /// Subscription ID
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub subscription_id: Option<i32>,
459
460    /// Regions with VPC peerings
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub regions: Option<Vec<ActiveActiveVpcRegion>>,
463}
464
465/// VPC Peering handler
466pub struct VpcPeeringHandler {
467    client: CloudClient,
468}
469
470impl VpcPeeringHandler {
471    /// Create a new VPC peering handler
472    #[must_use]
473    pub fn new(client: CloudClient) -> Self {
474        Self { client }
475    }
476
477    // ========================================================================
478    // Standard VPC Peering
479    // ========================================================================
480
481    /// Get VPC peering for subscription
482    pub async fn get(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
483        self.client
484            .get(&format!("/subscriptions/{subscription_id}/peerings"))
485            .await
486    }
487
488    /// Create VPC peering
489    pub async fn create(
490        &self,
491        subscription_id: i32,
492        request: &VpcPeeringCreateRequest,
493    ) -> Result<TaskStateUpdate> {
494        self.client
495            .post(
496                &format!("/subscriptions/{subscription_id}/peerings"),
497                request,
498            )
499            .await
500    }
501
502    /// Delete VPC peering
503    pub async fn delete(&self, subscription_id: i32, peering_id: i32) -> Result<TaskStateUpdate> {
504        self.client
505            .delete_typed(&format!(
506                "/subscriptions/{subscription_id}/peerings/{peering_id}"
507            ))
508            .await
509    }
510
511    /// Update VPC peering
512    pub async fn update(
513        &self,
514        subscription_id: i32,
515        peering_id: i32,
516        request: &VpcPeeringCreateRequest,
517    ) -> Result<TaskStateUpdate> {
518        self.client
519            .put(
520                &format!("/subscriptions/{subscription_id}/peerings/{peering_id}"),
521                request,
522            )
523            .await
524    }
525
526    // ========================================================================
527    // Active-Active VPC Peering
528    // ========================================================================
529    //
530    // Active-Active subscriptions peer each region independently under
531    // `/subscriptions/{subscriptionId}/regions/peerings`, a distinct surface
532    // from the standard VPC peering endpoints above.
533
534    /// Get Active-Active VPC peerings for a subscription.
535    ///
536    /// GET /subscriptions/{subscriptionId}/regions/peerings
537    pub async fn get_active_active(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
538        self.client
539            .get(&format!(
540                "/subscriptions/{subscription_id}/regions/peerings"
541            ))
542            .await
543    }
544
545    /// Create an Active-Active VPC peering.
546    ///
547    /// POST /subscriptions/{subscriptionId}/regions/peerings
548    ///
549    /// Use [`ActiveActiveVpcPeeringCreateRequest::for_aws`] or
550    /// [`ActiveActiveVpcPeeringCreateRequest::for_gcp`] to build a
551    /// provider-targeted body with the spec's required fields.
552    pub async fn create_active_active(
553        &self,
554        subscription_id: i32,
555        request: &ActiveActiveVpcPeeringCreateRequest,
556    ) -> Result<TaskStateUpdate> {
557        self.client
558            .post(
559                &format!("/subscriptions/{subscription_id}/regions/peerings"),
560                request,
561            )
562            .await
563    }
564
565    /// Update an Active-Active VPC peering's CIDR list.
566    ///
567    /// PUT /subscriptions/{subscriptionId}/regions/peerings/{peeringId}
568    pub async fn update_active_active(
569        &self,
570        subscription_id: i32,
571        peering_id: i32,
572        request: &VpcPeeringUpdateAwsRequest,
573    ) -> Result<TaskStateUpdate> {
574        self.client
575            .put(
576                &format!("/subscriptions/{subscription_id}/regions/peerings/{peering_id}"),
577                request,
578            )
579            .await
580    }
581
582    /// Delete an Active-Active VPC peering by its peering ID.
583    ///
584    /// DELETE /subscriptions/{subscriptionId}/regions/peerings/{peeringId}
585    pub async fn delete_active_active(
586        &self,
587        subscription_id: i32,
588        peering_id: i32,
589    ) -> Result<TaskStateUpdate> {
590        self.client
591            .delete_typed(&format!(
592                "/subscriptions/{subscription_id}/regions/peerings/{peering_id}"
593            ))
594            .await
595    }
596}