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}