Skip to main content

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_global_request(
84                Method::GET,
85                ["api", "region", &provider.cloud_provider, &provider.name],
86                None,
87            )
88            .await?;
89
90        match self.send_request::<Region>(req).await {
91            Ok(region) => match region.region_state {
92                RegionState::SoftDeleted => Err(Error::EmptyRegion),
93                RegionState::DeletionPending => Err(Error::EmptyRegion),
94                RegionState::Enabled => Ok(region),
95                RegionState::EnablementPending => Ok(region),
96            },
97            Err(Error::SuccesfullButNoContent) => Err(Error::EmptyRegion),
98            Err(e) => Err(e),
99        }
100    }
101
102    /// Get all the available customer regions for the current user.
103    pub async fn get_all_regions(&self) -> Result<Vec<Region>, Error> {
104        let cloud_providers: Vec<CloudProvider> = self.list_cloud_regions().await?;
105        let mut regions: Vec<Region> = vec![];
106
107        for cloud_provider in cloud_providers {
108            match self.get_region(cloud_provider).await {
109                Ok(region) => {
110                    regions.push(region);
111                }
112                // Skip cloud regions with no customer region
113                Err(Error::EmptyRegion) => {}
114                Err(e) => return Err(e),
115            }
116        }
117
118        Ok(regions)
119    }
120
121    /// Creates a customer region in a particular cloud region for the current user
122    pub async fn create_region(
123        &self,
124        version: Option<String>,
125        environmentd_extra_args: Vec<String>,
126        environmentd_cpu_allocation: Option<String>,
127        environmentd_memory_allocation: Option<String>,
128        cloud_provider: CloudProvider,
129    ) -> Result<Region, Error> {
130        #[derive(Serialize)]
131        #[serde(rename_all = "camelCase")]
132        struct Body {
133            #[serde(skip_serializing_if = "Option::is_none")]
134            environmentd_image_ref: Option<String>,
135            #[serde(skip_serializing_if = "Vec::is_empty")]
136            environmentd_extra_args: Vec<String>,
137            #[serde(skip_serializing_if = "Option::is_none")]
138            environmentd_cpu_allocation: Option<String>,
139            #[serde(skip_serializing_if = "Option::is_none")]
140            environmentd_memory_allocation: Option<String>,
141        }
142
143        let body = Body {
144            environmentd_image_ref: version.map(|v| match v.split_once(':') {
145                None => format!("materialize/environmentd:{v}"),
146                Some((user, v)) => format!("{user}/environmentd:{v}"),
147            }),
148            environmentd_extra_args,
149            environmentd_cpu_allocation,
150            environmentd_memory_allocation,
151        };
152
153        let req = self
154            .build_global_request(
155                Method::PATCH,
156                [
157                    "api",
158                    "region",
159                    &cloud_provider.cloud_provider,
160                    &cloud_provider.name,
161                ],
162                None,
163            )
164            .await?;
165        let req = req.json(&body);
166        // Creating a region can take some time
167        let req = req.timeout(Duration::from_secs(60));
168        self.send_request(req).await
169    }
170
171    /// Deletes a customer region in a particular cloud region for the current user.
172    ///
173    /// Soft deletes by default.
174    ///
175    /// NOTE that this operation is only available to Materialize employees
176    /// This operation has a long duration, it can take
177    /// several minutes to complete.
178    pub async fn delete_region(
179        &self,
180        cloud_provider: CloudProvider,
181        hard: bool,
182    ) -> Result<(), Error> {
183        /// A struct that deserializes nothing.
184        ///
185        /// Useful for deserializing empty response bodies.
186        struct Empty;
187
188        impl<'de> Deserialize<'de> for Empty {
189            fn deserialize<D>(_: D) -> Result<Empty, D::Error>
190            where
191                D: Deserializer<'de>,
192            {
193                Ok(Empty)
194            }
195        }
196
197        let query = if hard {
198            Some([("hardDelete", "true")].as_slice())
199        } else {
200            None
201        };
202
203        let req = self
204            .build_global_request(
205                Method::DELETE,
206                [
207                    "api",
208                    "region",
209                    &cloud_provider.cloud_provider,
210                    &cloud_provider.name,
211                ],
212                query,
213            )
214            .await?;
215        self.send_request::<Empty>(req).await?;
216
217        // Wait for the environment to be fully deleted
218        for _ in 0..600 {
219            let req = self
220                .build_global_request(
221                    Method::GET,
222                    [
223                        "api",
224                        "region",
225                        &cloud_provider.cloud_provider,
226                        &cloud_provider.name,
227                    ],
228                    None,
229                )
230                .await?;
231            let res = self.send_request::<Region>(req).await;
232            if hard {
233                if let Err(Error::SuccesfullButNoContent) = res {
234                    return Ok(());
235                }
236            } else {
237                if let Ok(Region {
238                    region_state: RegionState::SoftDeleted,
239                    ..
240                }) = res
241                {
242                    return Ok(());
243                }
244            }
245
246            tokio::time::sleep(Duration::from_secs(1)).await;
247        }
248        Err(Error::TimeoutError)
249    }
250}