mz/command/
region.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Implementation of the `mz region` command.
17//!
18//! Consult the user-facing documentation for details.
19//!
20use std::time::Duration;
21
22use crate::{context::RegionContext, error::Error};
23
24use mz_cloud_api::client::{cloud_provider::CloudProvider, region::RegionState};
25use mz_ore::retry::Retry;
26use serde::{Deserialize, Serialize};
27use tabled::Tabled;
28
29/// Enable a region in the profile organization.
30///
31/// In cases where the organization has already enabled the region
32/// the command will try to run a version update. Resulting
33/// in a downtime for a short period.
34pub async fn enable(
35    cx: RegionContext,
36    version: Option<String>,
37    environmentd_extra_arg: Option<Vec<String>>,
38    environmentd_cpu_allocation: Option<String>,
39    environmentd_memory_allocation: Option<String>,
40) -> Result<(), Error> {
41    let loading_spinner = cx
42        .output_formatter()
43        .loading_spinner("Retrieving information...");
44    let cloud_provider = cx.get_cloud_provider().await?;
45
46    loading_spinner.set_message("Enabling the region...");
47
48    let environmentd_extra_arg: Vec<String> = environmentd_extra_arg.unwrap_or_else(Vec::new);
49
50    // Loop region creation.
51    // After 6 minutes it will timeout.
52    let _ = Retry::default()
53        .max_duration(Duration::from_secs(720))
54        .clamp_backoff(Duration::from_secs(1))
55        .retry_async(|_| async {
56            let _ = cx
57                .cloud_client()
58                .create_region(
59                    version.clone(),
60                    environmentd_extra_arg.clone(),
61                    environmentd_cpu_allocation.clone(),
62                    environmentd_memory_allocation.clone(),
63                    cloud_provider.clone(),
64                )
65                .await?;
66            Ok(())
67        })
68        .await
69        .map_err(|e| Error::TimeoutError(Box::new(e)))?;
70
71    loading_spinner.set_message("Waiting for the region to be online...");
72
73    // Loop retrieving the region and checking the SQL connection for 6 minutes.
74    // After 6 minutes it will timeout.
75    let _ = Retry::default()
76        .max_duration(Duration::from_secs(720))
77        .clamp_backoff(Duration::from_secs(1))
78        .retry_async(|_| async {
79            let region = cx.get_region().await?;
80
81            match region.region_state {
82                RegionState::EnablementPending => {
83                    loading_spinner.set_message("Waiting for the region to be ready...");
84                    Err(Error::NotReadyRegion)
85                }
86                RegionState::DeletionPending => Err(Error::CommandExecutionError(
87                    "This region is pending deletion!".to_string(),
88                )),
89                RegionState::SoftDeleted => Err(Error::CommandExecutionError(
90                    "This region has been marked soft-deleted!".to_string(),
91                )),
92                RegionState::Enabled => match region.region_info {
93                    Some(region_info) => {
94                        loading_spinner.set_message("Waiting for the region to be resolvable...");
95                        if region_info.resolvable {
96                            let claims = cx.admin_client().claims().await?;
97                            let user = claims.user()?;
98                            if cx.sql_client().is_ready(&region_info, user)? {
99                                return Ok(());
100                            }
101                            Err(Error::NotPgReadyError)
102                        } else {
103                            Err(Error::NotResolvableRegion)
104                        }
105                    }
106                    None => Err(Error::NotReadyRegion),
107                },
108            }
109        })
110        .await
111        .map_err(|e| Error::TimeoutError(Box::new(e)))?;
112
113    loading_spinner.finish_with_message(format!("Region in {} is now online", cloud_provider.id));
114
115    Ok(())
116}
117
118/// Disable a region in the profile organization.
119///
120/// This command can take several minutes to complete.
121pub async fn disable(cx: RegionContext, hard: bool) -> Result<(), Error> {
122    let loading_spinner = cx
123        .output_formatter()
124        .loading_spinner("Retrieving information...");
125
126    let cloud_provider = cx.get_cloud_provider().await?;
127
128    // The `delete_region` method retries disabling a region,
129    // has an inner timeout, and manages a `504` response.
130    // For any other type of error response, we handle it here
131    // with a retry loop.
132    Retry::default()
133        .max_duration(Duration::from_secs(720))
134        .clamp_backoff(Duration::from_secs(1))
135        .retry_async(|_| async {
136            loading_spinner.set_message("Disabling region...");
137            cx.cloud_client()
138                .delete_region(cloud_provider.clone(), hard)
139                .await?;
140
141            loading_spinner.finish_with_message("Region disabled.");
142            Ok(())
143        })
144        .await
145}
146
147/// Lists all the available regions and their status.
148pub async fn list(cx: RegionContext) -> Result<(), Error> {
149    let output_formatter = cx.output_formatter();
150    let loading_spinner = output_formatter.loading_spinner("Retrieving regions...");
151
152    #[derive(Deserialize, Serialize, Tabled)]
153    pub struct Region<'a> {
154        #[tabled(rename = "Region")]
155        region: String,
156        #[tabled(rename = "Status")]
157        status: &'a str,
158    }
159
160    let cloud_providers: Vec<CloudProvider> = cx.cloud_client().list_cloud_regions().await?;
161    let mut regions: Vec<Region> = vec![];
162
163    for cloud_provider in cloud_providers {
164        match cx.cloud_client().get_region(cloud_provider.clone()).await {
165            Ok(_) => regions.push(Region {
166                region: cloud_provider.id,
167                status: "enabled",
168            }),
169            Err(mz_cloud_api::error::Error::EmptyRegion) => regions.push(Region {
170                region: cloud_provider.id,
171                status: "disabled",
172            }),
173            Err(err) => {
174                println!("Error: {:?}", err)
175            }
176        }
177    }
178
179    loading_spinner.finish_and_clear();
180    output_formatter.output_table(regions)?;
181    Ok(())
182}
183
184/// Shows the health of the profile region followed by the HTTP and SQL endpoints.
185pub async fn show(cx: RegionContext) -> Result<(), Error> {
186    // Sharing the reference of the context in multiple places makes
187    // it necesarry to wrap in an `alloc::rc`.
188
189    let output_formatter = cx.output_formatter();
190    let loading_spinner = output_formatter.loading_spinner("Retrieving region...");
191
192    let region_info = cx.get_region_info().await?;
193
194    loading_spinner.set_message("Checking environment health...");
195    let claims = cx.admin_client().claims().await?;
196    let sql_client = cx.sql_client();
197    let environment_health = match sql_client.is_ready(&region_info, claims.user()?) {
198        Ok(healthy) => match healthy {
199            true => "yes",
200            _ => "no",
201        },
202        Err(_) => "no",
203    };
204
205    loading_spinner.finish_and_clear();
206    output_formatter.output_scalar(Some(&format!("Healthy: \t{}", environment_health)))?;
207    output_formatter.output_scalar(Some(&format!("SQL address: \t{}", region_info.sql_address)))?;
208    output_formatter.output_scalar(Some(&format!("HTTP URL: \t{}", region_info.http_address)))?;
209
210    Ok(())
211}