redis-cloud 0.10.0

Redis Cloud REST API client library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
//! VPC peering operations for AWS and GCP Pro subscriptions.
//!
//! Manages VPC peering connections between a Redis Cloud subscription's
//! VPC and a customer-owned VPC, covering both standard subscriptions and
//! Active-Active (CRDB) subscriptions where each region is peered
//! independently.
//!
//! # When to use this module
//!
//! - You want direct VPC-to-VPC private connectivity (no public
//!   endpoint, no shared TGW).
//! - The subscription is on **AWS** or **GCP**. Azure connectivity is
//!   handled separately by the Redis Cloud console; the SDK does not
//!   yet expose Azure-specific endpoints here.
//!
//! For AWS hub-and-spoke topologies see
//! [`crate::connectivity::transit_gateway`]; for AWS endpoint-style
//! private connectivity see [`crate::connectivity::private_link`]; for
//! GCP endpoint-style private connectivity see
//! [`crate::connectivity::psc`].
//!
//! # Endpoint surface
//!
//! - `GET    /subscriptions/{subscriptionId}/peerings`
//! - `POST   /subscriptions/{subscriptionId}/peerings`
//! - `PUT    /subscriptions/{subscriptionId}/peerings/{peeringId}`
//! - `DELETE /subscriptions/{subscriptionId}/peerings/{peeringId}`
//!
//! Active-Active subscriptions peer each region independently under
//! `/subscriptions/{subscriptionId}/regions/peerings[/{peeringId}]`.
//!
//! # Example
//!
//! Construct a provider-targeted body and create a peering:
//!
//! ```rust,no_run
//! use redis_cloud::{CloudClient, VpcPeeringHandler};
//! use redis_cloud::connectivity::VpcPeeringCreateRequest;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let client = CloudClient::builder()
//!     .api_key("k").api_secret("s").build()?;
//! let handler = VpcPeeringHandler::new(client);
//!
//! let mut request = VpcPeeringCreateRequest::for_aws(
//!     "us-east-1", "123456789012", "vpc-12345678",
//! );
//! request.vpc_cidr = Some("10.0.0.0/16".to_string());
//! let task = handler.create(123, &request).await?;
//! # let _ = task;
//! # Ok(())
//! # }
//! ```
//!
//! # Errors
//!
//! All operations return [`crate::Result`]; transport, auth, and 4xx/5xx
//! responses surface as the corresponding [`crate::CloudError`] variant.

use crate::{CloudClient, Result};
use serde::{Deserialize, Serialize};

/// VPC peering creation request.
///
/// The Redis Cloud API documents this as a `oneOf` between an AWS-shaped
/// body (requiring `region`, `awsAccountId`, `vpcId`) and a GCP-shaped body
/// (requiring `vpcProjectUid`, `vpcNetworkName`). This struct keeps both
/// providers in one type for caller flexibility, but uses
/// `#[serde(rename = ...)]` so the AWS and GCP fields serialize to the
/// **exact wire names the spec requires**. Use [`Self::for_aws`] or
/// [`Self::for_gcp`] to construct provider-targeted bodies that avoid
/// mixing fields.
///
/// A type-safe enum split that prevents AWS+GCP field mixing at compile
/// time is tracked as a follow-on under #65.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VpcPeeringCreateRequest {
    /// Cloud provider discriminator (e.g. "AWS", "GCP").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,

    /// Read-only on the response; populated by the server.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command_type: Option<String>,

    // ------- AWS body -------
    /// AWS region. Wire name: `region` (spec required for AWS).
    #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
    pub aws_region: Option<String>,

    /// AWS account ID (spec required for AWS).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aws_account_id: Option<String>,

    /// AWS VPC ID (spec required for AWS).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_id: Option<String>,

    /// VPC CIDR. AWS only; optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// List of VPC CIDRs. AWS only; optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidrs: Option<Vec<String>>,

    // ------- GCP body -------
    /// GCP project UID. Wire name: `vpcProjectUid` (spec required for GCP).
    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
    pub gcp_project_id: Option<String>,

    /// GCP network name. Wire name: `vpcNetworkName` (spec required for GCP).
    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
    pub network_name: Option<String>,
}

impl VpcPeeringCreateRequest {
    /// Construct an AWS-targeted VPC peering creation body.
    ///
    /// Pre-populates `provider = "AWS"` and the three required AWS fields
    /// (`region`, `awsAccountId`, `vpcId`). Optional CIDR fields can be set
    /// directly on the returned struct.
    #[must_use]
    pub fn for_aws(
        region: impl Into<String>,
        aws_account_id: impl Into<String>,
        vpc_id: impl Into<String>,
    ) -> Self {
        Self {
            provider: Some("AWS".to_string()),
            aws_region: Some(region.into()),
            aws_account_id: Some(aws_account_id.into()),
            vpc_id: Some(vpc_id.into()),
            ..Self::default()
        }
    }

    /// Construct a GCP-targeted VPC peering creation body.
    ///
    /// Pre-populates `provider = "GCP"` and the two required GCP fields
    /// (`vpcProjectUid`, `vpcNetworkName`).
    #[must_use]
    pub fn for_gcp(project_uid: impl Into<String>, network_name: impl Into<String>) -> Self {
        Self {
            provider: Some("GCP".to_string()),
            gcp_project_id: Some(project_uid.into()),
            network_name: Some(network_name.into()),
            ..Self::default()
        }
    }
}

/// Base VPC peering creation request (for backward compatibility)
pub type VpcPeeringCreateBaseRequest = VpcPeeringCreateRequest;

/// Active-Active VPC peering creation request.
///
/// The Redis Cloud API documents this as a `oneOf` between an AWS-shaped body
/// (requiring `sourceRegion`, `destinationRegion`, `awsAccountId`, `vpcId`) and
/// a GCP-shaped body (requiring `sourceRegion`, `vpcProjectUid`,
/// `vpcNetworkName`). Like [`VpcPeeringCreateRequest`], both providers live in
/// one struct and use `#[serde(rename = ...)]` so each field serializes to the
/// **exact wire name the spec requires**. Use [`Self::for_aws`] or
/// [`Self::for_gcp`] to construct provider-targeted bodies that avoid mixing
/// fields.
///
/// A type-safe enum split that prevents AWS+GCP field mixing at compile time is
/// tracked as a follow-on under #65.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveActiveVpcPeeringCreateRequest {
    /// Cloud provider discriminator (e.g. "AWS", "GCP").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,

    /// Read-only on the response; populated by the server.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command_type: Option<String>,

    /// Name of the region to create the VPC peering from. Required for both
    /// providers.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_region: Option<String>,

    // ------- AWS body -------
    /// Name of the region to create the VPC peering to. AWS only; required.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub destination_region: Option<String>,

    /// AWS account ID (spec required for AWS).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aws_account_id: Option<String>,

    /// AWS VPC ID (spec required for AWS).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_id: Option<String>,

    /// VPC CIDR. AWS only; optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// List of VPC CIDRs. AWS only; optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidrs: Option<Vec<String>>,

    // ------- GCP body -------
    /// GCP project UID. Wire name: `vpcProjectUid` (spec required for GCP).
    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
    pub gcp_project_id: Option<String>,

    /// GCP network name. Wire name: `vpcNetworkName` (spec required for GCP).
    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
    pub network_name: Option<String>,
}

impl ActiveActiveVpcPeeringCreateRequest {
    /// Construct an AWS-targeted Active-Active VPC peering creation body.
    ///
    /// Pre-populates `provider = "AWS"` and the four required AWS fields
    /// (`sourceRegion`, `destinationRegion`, `awsAccountId`, `vpcId`). Optional
    /// CIDR fields can be set directly on the returned struct.
    #[must_use]
    pub fn for_aws(
        source_region: impl Into<String>,
        destination_region: impl Into<String>,
        aws_account_id: impl Into<String>,
        vpc_id: impl Into<String>,
    ) -> Self {
        Self {
            provider: Some("AWS".to_string()),
            source_region: Some(source_region.into()),
            destination_region: Some(destination_region.into()),
            aws_account_id: Some(aws_account_id.into()),
            vpc_id: Some(vpc_id.into()),
            ..Self::default()
        }
    }

    /// Construct a GCP-targeted Active-Active VPC peering creation body.
    ///
    /// Pre-populates `provider = "GCP"` and the three required GCP fields
    /// (`sourceRegion`, `vpcProjectUid`, `vpcNetworkName`).
    #[must_use]
    pub fn for_gcp(
        source_region: impl Into<String>,
        project_uid: impl Into<String>,
        network_name: impl Into<String>,
    ) -> Self {
        Self {
            provider: Some("GCP".to_string()),
            source_region: Some(source_region.into()),
            gcp_project_id: Some(project_uid.into()),
            network_name: Some(network_name.into()),
            ..Self::default()
        }
    }
}

/// VPC peering update request for AWS
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VpcPeeringUpdateAwsRequest {
    /// Subscription that owns the peering. Server-populated from the path.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscription_id: Option<i32>,

    /// VPC Peering ID to update.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_peering_id: Option<i32>,

    /// Optional. VPC CIDR.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// Optional. List of VPC CIDRs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidrs: Option<Vec<String>>,

    /// Read-only on the response; populated by the server with the
    /// operation type (e.g. `"UPDATE_VPC_PEERING"`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command_type: Option<String>,
}

/// VPC peering update request (generic)
pub type VpcPeeringUpdateRequest = VpcPeeringUpdateAwsRequest;

/// Task state update response
pub use crate::types::TaskStateUpdate;

/// VPC CIDR with status
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VpcCidr {
    /// VPC CIDR block
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// CIDR status (active/inactive)
    #[serde(rename = "active", skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

/// VPC Peering information
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VpcPeering {
    /// VPC Peering ID
    #[serde(rename = "vpcPeeringId", skip_serializing_if = "Option::is_none")]
    pub id: Option<i32>,

    /// Peering status (e.g., "active", "pending-acceptance")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,

    /// AWS account ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aws_account_id: Option<String>,

    /// AWS VPC peering connection ID
    #[serde(rename = "awsPeeringUid", skip_serializing_if = "Option::is_none")]
    pub aws_peering_id: Option<String>,

    /// VPC ID
    #[serde(rename = "vpcUid", skip_serializing_if = "Option::is_none")]
    pub vpc_id: Option<String>,

    /// VPC CIDR
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// List of VPC CIDRs with status
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidrs: Option<Vec<VpcCidr>>,

    /// GCP project UID
    #[serde(rename = "projectUid", skip_serializing_if = "Option::is_none")]
    pub gcp_project_uid: Option<String>,

    /// GCP network name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub network_name: Option<String>,

    /// Redis GCP project UID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redis_project_uid: Option<String>,

    /// Redis GCP network name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redis_network_name: Option<String>,

    /// Cloud peering ID (GCP)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cloud_peering_id: Option<String>,

    /// Cloud provider region
    #[serde(rename = "regionName", skip_serializing_if = "Option::is_none")]
    pub region: Option<String>,

    /// Cloud provider (AWS, GCP, Azure)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
}

/// Active-Active VPC Peering information
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveActiveVpcPeering {
    /// VPC Peering ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<i32>,

    /// Peering status
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,

    /// Region ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub region_id: Option<i32>,

    /// Region name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub region_name: Option<String>,

    /// AWS account ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aws_account_id: Option<String>,

    /// AWS VPC peering UID
    #[serde(rename = "awsPeeringUid", skip_serializing_if = "Option::is_none")]
    pub aws_peering_id: Option<String>,

    /// VPC UID
    #[serde(rename = "vpcUid", skip_serializing_if = "Option::is_none")]
    pub vpc_id: Option<String>,

    /// VPC CIDR
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidr: Option<String>,

    /// List of VPC CIDRs with status
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_cidrs: Option<Vec<VpcCidr>>,

    /// GCP project UID
    #[serde(rename = "vpcProjectUid", skip_serializing_if = "Option::is_none")]
    pub gcp_project_uid: Option<String>,

    /// GCP network name
    #[serde(rename = "vpcNetworkName", skip_serializing_if = "Option::is_none")]
    pub network_name: Option<String>,

    /// Redis GCP project UID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redis_project_uid: Option<String>,

    /// Redis GCP network name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redis_network_name: Option<String>,

    /// Cloud peering ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cloud_peering_id: Option<String>,

    /// Source region
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_region: Option<String>,

    /// Destination region
    #[serde(skip_serializing_if = "Option::is_none")]
    pub destination_region: Option<String>,
}

/// Active-Active VPC Peering region
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveActiveVpcRegion {
    /// Region ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<i32>,

    /// Source region name
    #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
    pub source_region: Option<String>,

    /// VPC Peerings in this region
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vpc_peerings: Option<Vec<ActiveActiveVpcPeering>>,
}

/// Active-Active VPC Peering list response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveActiveVpcPeeringList {
    /// Subscription ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscription_id: Option<i32>,

    /// Regions with VPC peerings
    #[serde(skip_serializing_if = "Option::is_none")]
    pub regions: Option<Vec<ActiveActiveVpcRegion>>,
}

/// VPC Peering handler
pub struct VpcPeeringHandler {
    client: CloudClient,
}

impl VpcPeeringHandler {
    /// Create a new VPC peering handler
    #[must_use]
    pub fn new(client: CloudClient) -> Self {
        Self { client }
    }

    // ========================================================================
    // Standard VPC Peering
    // ========================================================================

    /// Get VPC peering for subscription
    pub async fn get(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
        self.client
            .get(&format!("/subscriptions/{subscription_id}/peerings"))
            .await
    }

    /// Create VPC peering
    pub async fn create(
        &self,
        subscription_id: i32,
        request: &VpcPeeringCreateRequest,
    ) -> Result<TaskStateUpdate> {
        self.client
            .post(
                &format!("/subscriptions/{subscription_id}/peerings"),
                request,
            )
            .await
    }

    /// Delete VPC peering
    pub async fn delete(&self, subscription_id: i32, peering_id: i32) -> Result<TaskStateUpdate> {
        self.client
            .delete_typed(&format!(
                "/subscriptions/{subscription_id}/peerings/{peering_id}"
            ))
            .await
    }

    /// Update VPC peering
    pub async fn update(
        &self,
        subscription_id: i32,
        peering_id: i32,
        request: &VpcPeeringCreateRequest,
    ) -> Result<TaskStateUpdate> {
        self.client
            .put(
                &format!("/subscriptions/{subscription_id}/peerings/{peering_id}"),
                request,
            )
            .await
    }

    // ========================================================================
    // Active-Active VPC Peering
    // ========================================================================
    //
    // Active-Active subscriptions peer each region independently under
    // `/subscriptions/{subscriptionId}/regions/peerings`, a distinct surface
    // from the standard VPC peering endpoints above.

    /// Get Active-Active VPC peerings for a subscription.
    ///
    /// GET /subscriptions/{subscriptionId}/regions/peerings
    pub async fn get_active_active(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
        self.client
            .get(&format!(
                "/subscriptions/{subscription_id}/regions/peerings"
            ))
            .await
    }

    /// Create an Active-Active VPC peering.
    ///
    /// POST /subscriptions/{subscriptionId}/regions/peerings
    ///
    /// Use [`ActiveActiveVpcPeeringCreateRequest::for_aws`] or
    /// [`ActiveActiveVpcPeeringCreateRequest::for_gcp`] to build a
    /// provider-targeted body with the spec's required fields.
    pub async fn create_active_active(
        &self,
        subscription_id: i32,
        request: &ActiveActiveVpcPeeringCreateRequest,
    ) -> Result<TaskStateUpdate> {
        self.client
            .post(
                &format!("/subscriptions/{subscription_id}/regions/peerings"),
                request,
            )
            .await
    }

    /// Update an Active-Active VPC peering's CIDR list.
    ///
    /// PUT /subscriptions/{subscriptionId}/regions/peerings/{peeringId}
    pub async fn update_active_active(
        &self,
        subscription_id: i32,
        peering_id: i32,
        request: &VpcPeeringUpdateAwsRequest,
    ) -> Result<TaskStateUpdate> {
        self.client
            .put(
                &format!("/subscriptions/{subscription_id}/regions/peerings/{peering_id}"),
                request,
            )
            .await
    }

    /// Delete an Active-Active VPC peering by its peering ID.
    ///
    /// DELETE /subscriptions/{subscriptionId}/regions/peerings/{peeringId}
    pub async fn delete_active_active(
        &self,
        subscription_id: i32,
        peering_id: i32,
    ) -> Result<TaskStateUpdate> {
        self.client
            .delete_typed(&format!(
                "/subscriptions/{subscription_id}/regions/peerings/{peering_id}"
            ))
            .await
    }
}