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        environmentd_cpu_allocation: Option<String>,
123        environmentd_memory_allocation: Option<String>,
124        cloud_provider: CloudProvider,
125    ) -> Result<Region, Error> {
126        #[derive(Serialize)]
127        #[serde(rename_all = "camelCase")]
128        struct Body {
129            #[serde(skip_serializing_if = "Option::is_none")]
130            environmentd_image_ref: Option<String>,
131            #[serde(skip_serializing_if = "Vec::is_empty")]
132            environmentd_extra_args: Vec<String>,
133            #[serde(skip_serializing_if = "Option::is_none")]
134            environmentd_cpu_allocation: Option<String>,
135            #[serde(skip_serializing_if = "Option::is_none")]
136            environmentd_memory_allocation: Option<String>,
137        }
138
139        let body = Body {
140            environmentd_image_ref: version.map(|v| match v.split_once(':') {
141                None => format!("materialize/environmentd:{v}"),
142                Some((user, v)) => format!("{user}/environmentd:{v}"),
143            }),
144            environmentd_extra_args,
145            environmentd_cpu_allocation,
146            environmentd_memory_allocation,
147        };
148
149        let req = self
150            .build_region_request(
151                Method::PATCH,
152                ["api", "region"],
153                None,
154                &cloud_provider,
155                Some(1),
156            )
157            .await?;
158        let req = req.json(&body);
159        // Creating a region can take some time
160        let req = req.timeout(Duration::from_secs(60));
161        self.send_request(req).await
162    }
163
164    /// Deletes a customer region in a particular cloud region for the current user.
165    ///
166    /// Soft deletes by default.
167    ///
168    /// NOTE that this operation is only available to Materialize employees
169    /// This operation has a long duration, it can take
170    /// several minutes to complete.
171    pub async fn delete_region(
172        &self,
173        cloud_provider: CloudProvider,
174        hard: bool,
175    ) -> Result<(), Error> {
176        /// A struct that deserializes nothing.
177        ///
178        /// Useful for deserializing empty response bodies.
179        struct Empty;
180
181        impl<'de> Deserialize<'de> for Empty {
182            fn deserialize<D>(_: D) -> Result<Empty, D::Error>
183            where
184                D: Deserializer<'de>,
185            {
186                Ok(Empty)
187            }
188        }
189
190        let query = if hard {
191            Some([("hardDelete", "true")].as_slice())
192        } else {
193            None
194        };
195
196        let req = self
197            .build_region_request(
198                Method::DELETE,
199                ["api", "region"],
200                query,
201                &cloud_provider,
202                Some(1),
203            )
204            .await?;
205        self.send_request::<Empty>(req).await?;
206
207        // Wait for the environment to be fully deleted
208        for _ in 0..600 {
209            let req = self
210                .build_region_request(
211                    Method::GET,
212                    ["api", "region"],
213                    None,
214                    &cloud_provider,
215                    Some(1),
216                )
217                .await?;
218            let res = self.send_request::<Region>(req).await;
219            if hard {
220                if let Err(Error::SuccesfullButNoContent) = res {
221                    return Ok(());
222                }
223            } else {
224                if let Ok(Region {
225                    region_state: RegionState::SoftDeleted,
226                    ..
227                }) = res
228                {
229                    return Ok(());
230                }
231            }
232
233            tokio::time::sleep(Duration::from_secs(1)).await;
234        }
235        Err(Error::TimeoutError)
236    }
237}