mz_cloud_api/client/
region.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! This module implements Materialize cloud API methods
11//! to GET, CREATE or DELETE a region.
12//! To delete an region correctly make sure to
13//! contact support.
14//!
15//! For a better experience retrieving all the available
16//! environments, use [`Client::get_all_regions()`]
17
18use std::time::Duration;
19
20use chrono::{DateTime, Utc};
21use reqwest::Method;
22use serde::{Deserialize, Deserializer, Serialize};
23
24use crate::client::cloud_provider::CloudProvider;
25use crate::client::{Client, Error};
26
27/// A customer region is represented in this structure
28#[derive(Debug, Deserialize, Clone)]
29#[serde(rename_all = "camelCase")]
30pub struct Region {
31    /// The connection info and metadata corresponding to this Region
32    /// may not be set if the region is in the process
33    /// of being created (see [RegionState] for details)
34    pub region_info: Option<RegionInfo>,
35
36    /// The state of this Region
37    pub region_state: RegionState,
38}
39
40/// Connection details for an active region
41#[derive(Debug, Deserialize, Clone)]
42#[serde(rename_all = "camelCase")]
43pub struct RegionInfo {
44    /// Represents the environmentd PG wire protocol address.
45    ///
46    /// E.g.: 3es24sg5rghjku7josdcs5jd7.eu-west-1.aws.materialize.cloud:6875
47    pub sql_address: String,
48    /// Represents the environmentd HTTP address.
49    ///
50    /// E.g.: 3es24sg5rghjku7josdcs5jd7.eu-west-1.aws.materialize.cloud:443
51    pub http_address: String,
52    /// Indicates true if the address is resolvable by DNS.
53    pub resolvable: bool,
54    /// The time at which the region was enabled
55    pub enabled_at: Option<DateTime<Utc>>,
56}
57
58/// The state of a customer region
59#[derive(Debug, Deserialize, Clone)]
60#[serde(rename_all = "kebab-case")]
61pub enum RegionState {
62    /// Enabled region
63    Enabled,
64
65    /// Enablement Pending
66    /// [region_info][Region::region_info] field will be `null` while the region is in this state
67    EnablementPending,
68
69    /// Deletion Pending
70    /// [region_info][Region::region_info] field will be `null` while the region is in this state
71    DeletionPending,
72
73    /// Soft deleted; Pending hard deletion
74    /// [region_info][Region::region_info] field will be `null` while the region is in this state
75    SoftDeleted,
76}
77
78impl Client {
79    /// Get a customer region in a partciular cloud region for the current user.
80    pub async fn get_region(&self, provider: CloudProvider) -> Result<Region, Error> {
81        // Send request to the subdomain
82        let req = self
83            .build_region_request(Method::GET, ["api", "region"], None, &provider, Some(1))
84            .await?;
85
86        match self.send_request::<Region>(req).await {
87            Ok(region) => match region.region_state {
88                RegionState::SoftDeleted => Err(Error::EmptyRegion),
89                RegionState::DeletionPending => Err(Error::EmptyRegion),
90                RegionState::Enabled => Ok(region),
91                RegionState::EnablementPending => Ok(region),
92            },
93            Err(Error::SuccesfullButNoContent) => Err(Error::EmptyRegion),
94            Err(e) => Err(e),
95        }
96    }
97
98    /// Get all the available customer regions for the current user.
99    pub async fn get_all_regions(&self) -> Result<Vec<Region>, Error> {
100        let cloud_providers: Vec<CloudProvider> = self.list_cloud_regions().await?;
101        let mut regions: Vec<Region> = vec![];
102
103        for cloud_provider in cloud_providers {
104            match self.get_region(cloud_provider).await {
105                Ok(region) => {
106                    regions.push(region);
107                }
108                // Skip cloud regions with no customer region
109                Err(Error::EmptyRegion) => {}
110                Err(e) => return Err(e),
111            }
112        }
113
114        Ok(regions)
115    }
116
117    /// Creates a customer region in a particular cloud region for the current user
118    pub async fn create_region(
119        &self,
120        version: Option<String>,
121        environmentd_extra_args: Vec<String>,
122        cloud_provider: CloudProvider,
123    ) -> Result<Region, Error> {
124        #[derive(Serialize)]
125        #[serde(rename_all = "camelCase")]
126        struct Body {
127            #[serde(skip_serializing_if = "Option::is_none")]
128            environmentd_image_ref: Option<String>,
129            #[serde(skip_serializing_if = "Vec::is_empty")]
130            environmentd_extra_args: Vec<String>,
131        }
132
133        let body = Body {
134            environmentd_image_ref: version.map(|v| match v.split_once(':') {
135                None => format!("materialize/environmentd:{v}"),
136                Some((user, v)) => format!("{user}/environmentd:{v}"),
137            }),
138            environmentd_extra_args,
139        };
140
141        let req = self
142            .build_region_request(
143                Method::PATCH,
144                ["api", "region"],
145                None,
146                &cloud_provider,
147                Some(1),
148            )
149            .await?;
150        let req = req.json(&body);
151        // Creating a region can take some time
152        let req = req.timeout(Duration::from_secs(60));
153        self.send_request(req).await
154    }
155
156    /// Deletes a customer region in a particular cloud region for the current user.
157    ///
158    /// Soft deletes by default.
159    ///
160    /// NOTE that this operation is only available to Materialize employees
161    /// This operation has a long duration, it can take
162    /// several minutes to complete.
163    pub async fn delete_region(
164        &self,
165        cloud_provider: CloudProvider,
166        hard: bool,
167    ) -> Result<(), Error> {
168        /// A struct that deserializes nothing.
169        ///
170        /// Useful for deserializing empty response bodies.
171        struct Empty;
172
173        impl<'de> Deserialize<'de> for Empty {
174            fn deserialize<D>(_: D) -> Result<Empty, D::Error>
175            where
176                D: Deserializer<'de>,
177            {
178                Ok(Empty)
179            }
180        }
181
182        let query = if hard {
183            Some([("hardDelete", "true")].as_slice())
184        } else {
185            None
186        };
187
188        let req = self
189            .build_region_request(
190                Method::DELETE,
191                ["api", "region"],
192                query,
193                &cloud_provider,
194                Some(1),
195            )
196            .await?;
197        self.send_request::<Empty>(req).await?;
198
199        // Wait for the environment to be fully deleted
200        for _ in 0..600 {
201            let req = self
202                .build_region_request(
203                    Method::GET,
204                    ["api", "region"],
205                    None,
206                    &cloud_provider,
207                    Some(1),
208                )
209                .await?;
210            let res = self.send_request::<Region>(req).await;
211            if hard {
212                if let Err(Error::SuccesfullButNoContent) = res {
213                    return Ok(());
214                }
215            } else {
216                if let Ok(Region {
217                    region_state: RegionState::SoftDeleted,
218                    ..
219                }) = res
220                {
221                    return Ok(());
222                }
223            }
224
225            tokio::time::sleep(Duration::from_secs(1)).await;
226        }
227        Err(Error::TimeoutError)
228    }
229}