Skip to main content

redis_cloud/connectivity/
psc.rs

1//! Google Cloud Private Service Connect (PSC) operations.
2//!
3//! Manages Private Service Connect services and endpoints so Redis Cloud
4//! databases can be reached from a GCP VPC without traversing the public
5//! internet.
6//!
7//! # When to use this module
8//!
9//! - The subscription is on GCP and you want connectivity that does not
10//!   require a VPC peering connection or a public endpoint.
11//! - You manage multiple client projects and want each to attach via its
12//!   own consumer endpoint.
13//!
14//! For AWS connectivity see [`crate::connectivity::vpc_peering`] (general
15//! VPC peering) or [`crate::connectivity::private_link`] (AWS PrivateLink).
16//! For AWS hub-and-spoke topologies see
17//! [`crate::connectivity::transit_gateway`].
18//!
19//! # Endpoint surface
20//!
21//! Service-level (one per subscription / region):
22//!
23//! - `GET    /subscriptions/{subscriptionId}/private-service-connect`
24//! - `POST   /subscriptions/{subscriptionId}/private-service-connect`
25//! - `DELETE /subscriptions/{subscriptionId}/private-service-connect`
26//!
27//! Endpoint-level (consumer endpoints under the service):
28//!
29//! - `POST /subscriptions/{subscriptionId}/private-service-connect/.../endpoints`
30//! - `PUT  /subscriptions/{subscriptionId}/private-service-connect/.../endpoints/{endpointId}`
31//!
32//! Active-Active subscriptions expose the same surface scoped to a region
33//! id via `/subscriptions/{subscriptionId}/regions/{regionId}/...`.
34//!
35//! # Errors
36//!
37//! All operations return [`crate::Result`]; transport, auth, and 4xx/5xx
38//! responses surface as the corresponding [`crate::CloudError`] variant.
39
40use crate::{CloudClient, Result};
41use serde::{Deserialize, Serialize};
42
43/// Private Service Connect endpoint update request
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct PscEndpointUpdateRequest {
47    /// Subscription that owns the PSC service. Server-populated; clients
48    /// pass the value via the path parameter and may leave the default.
49    pub subscription_id: i32,
50    /// PSC service ID under the subscription. Server-populated.
51    pub psc_service_id: i32,
52    /// PSC endpoint ID being updated. Server-populated.
53    pub endpoint_id: i32,
54
55    /// Google Cloud project ID
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub gcp_project_id: Option<String>,
58
59    /// Name of the Google Cloud VPC that hosts your application
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub gcp_vpc_name: Option<String>,
62
63    /// Name of your VPC's subnet of IP address ranges
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub gcp_vpc_subnet_name: Option<String>,
66
67    /// Prefix used to create PSC endpoints in the consumer application VPC
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub endpoint_connection_name: Option<String>,
70}
71
72/// Task state update response
73pub use crate::types::TaskStateUpdate;
74
75/// Private Service Connect service information
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct PrivateServiceConnectService {
79    /// PSC service ID
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub id: Option<i32>,
82
83    /// Connection host name for the PSC service
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub connection_host_name: Option<String>,
86
87    /// GCP service attachment name
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub service_attachment_name: Option<String>,
90
91    /// PSC service status
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub status: Option<String>,
94}
95
96/// Private Service Connect endpoint information
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct PrivateServiceConnectEndpoint {
100    /// Endpoint ID
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub id: Option<i32>,
103
104    /// GCP project ID
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub gcp_project_id: Option<String>,
107
108    /// GCP VPC name
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub gcp_vpc_name: Option<String>,
111
112    /// GCP VPC subnet name
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub gcp_vpc_subnet_name: Option<String>,
115
116    /// Endpoint connection name
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub endpoint_connection_name: Option<String>,
119
120    /// Endpoint status
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub status: Option<String>,
123}
124
125/// Private Service Connect endpoints response
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct PrivateServiceConnectEndpoints {
129    /// PSC service ID
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub psc_service_id: Option<i32>,
132
133    /// List of PSC endpoints
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub endpoints: Option<Vec<PrivateServiceConnectEndpoint>>,
136}
137
138/// GCP creation script for PSC endpoint
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct GcpCreationScript {
142    /// Bash script for endpoint creation
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub bash: Option<String>,
145
146    /// `PowerShell` script for endpoint creation
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub powershell: Option<String>,
149
150    /// Terraform GCP configuration
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub terraform_gcp: Option<TerraformGcp>,
153}
154
155/// Terraform GCP configuration
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct TerraformGcp {
159    /// Service attachment configurations
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub service_attachments: Option<Vec<TerraformGcpServiceAttachment>>,
162}
163
164/// Terraform GCP service attachment configuration
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct TerraformGcpServiceAttachment {
168    /// Service attachment name
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub name: Option<String>,
171
172    /// DNS record
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub dns_record: Option<String>,
175
176    /// IP address name
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub ip_address_name: Option<String>,
179
180    /// Forwarding rule name
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub forwarding_rule_name: Option<String>,
183}
184
185/// GCP deletion script for PSC endpoint
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct GcpDeletionScript {
189    /// Bash script for endpoint deletion
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub bash: Option<String>,
192
193    /// `PowerShell` script for endpoint deletion
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub powershell: Option<String>,
196}
197
198/// Private Service Connect handler
199pub struct PscHandler {
200    client: CloudClient,
201}
202
203impl PscHandler {
204    /// Create a new PSC handler
205    #[must_use]
206    pub fn new(client: CloudClient) -> Self {
207        Self { client }
208    }
209
210    // ========================================================================
211    // Standard PSC Operations
212    // ========================================================================
213
214    /// Delete Private Service Connect service
215    pub async fn delete_service(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
216        self.client
217            .delete_typed(&format!(
218                "/subscriptions/{subscription_id}/private-service-connect"
219            ))
220            .await
221    }
222
223    /// Get Private Service Connect service
224    pub async fn get_service(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
225        self.client
226            .get(&format!(
227                "/subscriptions/{subscription_id}/private-service-connect"
228            ))
229            .await
230    }
231
232    /// Create Private Service Connect service
233    pub async fn create_service(&self, subscription_id: i32) -> Result<TaskStateUpdate> {
234        self.client
235            .post(
236                &format!("/subscriptions/{subscription_id}/private-service-connect"),
237                &serde_json::json!({}),
238            )
239            .await
240    }
241
242    /// Create a Private Service Connect endpoint under the given service.
243    pub async fn create_endpoint(
244        &self,
245        subscription_id: i32,
246        psc_service_id: i32,
247        request: &PscEndpointUpdateRequest,
248    ) -> Result<TaskStateUpdate> {
249        self.client
250            .post(
251                &format!(
252                    "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}"
253                ),
254                request,
255            )
256            .await
257    }
258
259    /// Get the endpoints of a Private Service Connect service.
260    ///
261    /// GET /subscriptions/{subscriptionId}/private-service-connect/{pscServiceId}
262    pub async fn get_endpoints(
263        &self,
264        subscription_id: i32,
265        psc_service_id: i32,
266    ) -> Result<TaskStateUpdate> {
267        self.client
268            .get(&format!(
269                "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}"
270            ))
271            .await
272    }
273
274    /// Delete Private Service Connect endpoint
275    pub async fn delete_endpoint(
276        &self,
277        subscription_id: i32,
278        psc_service_id: i32,
279        endpoint_id: i32,
280    ) -> Result<TaskStateUpdate> {
281        self.client
282            .delete_typed(&format!(
283                "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}"
284            ))
285            .await
286    }
287
288    /// Update Private Service Connect endpoint
289    pub async fn update_endpoint(
290        &self,
291        subscription_id: i32,
292        psc_service_id: i32,
293        endpoint_id: i32,
294        request: &PscEndpointUpdateRequest,
295    ) -> Result<TaskStateUpdate> {
296        self.client
297            .put(
298                &format!(
299                    "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}"
300                ),
301                request,
302            )
303            .await
304    }
305
306    /// Get PSC endpoint creation script
307    pub async fn get_endpoint_creation_script(
308        &self,
309        subscription_id: i32,
310        psc_service_id: i32,
311        endpoint_id: i32,
312    ) -> Result<String> {
313        self.client
314            .get(&format!(
315                "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}/creationScripts"
316            ))
317            .await
318    }
319
320    /// Get PSC endpoint deletion script
321    pub async fn get_endpoint_deletion_script(
322        &self,
323        subscription_id: i32,
324        psc_service_id: i32,
325        endpoint_id: i32,
326    ) -> Result<String> {
327        self.client
328            .get(&format!(
329                "/subscriptions/{subscription_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}/deletionScripts"
330            ))
331            .await
332    }
333
334    // ========================================================================
335    // Active-Active PSC Operations
336    // ========================================================================
337
338    /// Delete Active-Active PSC service for a region
339    pub async fn delete_service_active_active(
340        &self,
341        subscription_id: i32,
342        region_id: i32,
343    ) -> Result<TaskStateUpdate> {
344        self.client
345            .delete_typed(&format!(
346                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect"
347            ))
348            .await
349    }
350
351    /// Get Active-Active PSC service for a region
352    pub async fn get_service_active_active(
353        &self,
354        subscription_id: i32,
355        region_id: i32,
356    ) -> Result<TaskStateUpdate> {
357        self.client
358            .get(&format!(
359                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect"
360            ))
361            .await
362    }
363
364    /// Create Active-Active PSC service for a region
365    pub async fn create_service_active_active(
366        &self,
367        subscription_id: i32,
368        region_id: i32,
369    ) -> Result<TaskStateUpdate> {
370        self.client
371            .post(
372                &format!(
373                    "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect"
374                ),
375                &serde_json::json!({}),
376            )
377            .await
378    }
379
380    /// Create an Active-Active Private Service Connect endpoint under the
381    /// given service for a region.
382    pub async fn create_endpoint_active_active(
383        &self,
384        subscription_id: i32,
385        region_id: i32,
386        psc_service_id: i32,
387        request: &PscEndpointUpdateRequest,
388    ) -> Result<TaskStateUpdate> {
389        self.client
390            .post(
391                &format!(
392                    "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}"
393                ),
394                request,
395            )
396            .await
397    }
398
399    /// Get the endpoints of an Active-Active Private Service Connect service
400    /// for a region.
401    ///
402    /// GET /subscriptions/{subscriptionId}/regions/{regionId}/private-service-connect/{pscServiceId}
403    pub async fn get_endpoints_active_active(
404        &self,
405        subscription_id: i32,
406        region_id: i32,
407        psc_service_id: i32,
408    ) -> Result<TaskStateUpdate> {
409        self.client
410            .get(&format!(
411                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}"
412            ))
413            .await
414    }
415
416    /// Delete Active-Active PSC endpoint
417    pub async fn delete_endpoint_active_active(
418        &self,
419        subscription_id: i32,
420        region_id: i32,
421        psc_service_id: i32,
422        endpoint_id: i32,
423    ) -> Result<TaskStateUpdate> {
424        self.client.delete_typed(&format!(
425                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}"
426            )).await
427    }
428
429    /// Update Active-Active PSC endpoint
430    pub async fn update_endpoint_active_active(
431        &self,
432        subscription_id: i32,
433        region_id: i32,
434        psc_service_id: i32,
435        endpoint_id: i32,
436        request: &PscEndpointUpdateRequest,
437    ) -> Result<TaskStateUpdate> {
438        self.client
439            .put(
440                &format!(
441                    "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}"
442                ),
443                request,
444            )
445            .await
446    }
447
448    /// Get Active-Active PSC endpoint creation script
449    pub async fn get_endpoint_creation_script_active_active(
450        &self,
451        subscription_id: i32,
452        region_id: i32,
453        psc_service_id: i32,
454        endpoint_id: i32,
455    ) -> Result<String> {
456        self.client
457            .get(&format!(
458                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}/creationScripts"
459            ))
460            .await
461    }
462
463    /// Get Active-Active PSC endpoint deletion script
464    pub async fn get_endpoint_deletion_script_active_active(
465        &self,
466        subscription_id: i32,
467        region_id: i32,
468        psc_service_id: i32,
469        endpoint_id: i32,
470    ) -> Result<String> {
471        self.client
472            .get(&format!(
473                "/subscriptions/{subscription_id}/regions/{region_id}/private-service-connect/{psc_service_id}/endpoints/{endpoint_id}/deletionScripts"
474            ))
475            .await
476    }
477}