Skip to main content

mz_deploy/cli/commands/
debug.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//! Debug command - test database connection.
11
12use crate::cli::CliError;
13use crate::client::{Client, SERVER_CLUSTER_NAME};
14use crate::config::Settings;
15use crate::docker_runtime::{DockerRuntime, DockerStatus};
16use crate::log;
17use owo_colors::{OwoColorize, Stream};
18use std::fmt;
19
20/// Health of the `_mz_deploy_server` cluster as observed by `debug`.
21#[derive(Debug, Clone, serde::Serialize)]
22#[serde(rename_all = "snake_case", tag = "status")]
23pub enum ServerClusterHealth {
24    /// Cluster exists and has replication_factor > 0.
25    Healthy,
26    /// Cluster exists but is not usable (e.g., replication_factor == 0).
27    NotReady { reason: String },
28    /// Cluster is not present in `mz_catalog.mz_clusters`.
29    Missing,
30}
31
32async fn check_server_cluster(client: &Client) -> Result<ServerClusterHealth, CliError> {
33    match client
34        .introspection()
35        .get_cluster(SERVER_CLUSTER_NAME)
36        .await?
37    {
38        None => Ok(ServerClusterHealth::Missing),
39        Some(c) if c.replication_factor.unwrap_or(0) > 0 => Ok(ServerClusterHealth::Healthy),
40        Some(_) => Ok(ServerClusterHealth::NotReady {
41            reason: "replication factor is 0".into(),
42        }),
43    }
44}
45
46#[derive(serde::Serialize)]
47enum RemoteOutput {
48    Success {
49        environment_id: String,
50        server_cluster_health: ServerClusterHealth,
51    },
52
53    Failure {
54        host: String,
55        port: u16,
56    },
57}
58
59#[derive(serde::Serialize)]
60struct DebugOutput {
61    profile: String,
62    docker_status: String,
63    remote: RemoteOutput,
64}
65
66impl fmt::Display for DebugOutput {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        writeln!(
69            f,
70            "{}: {}",
71            "Profile".if_supports_color(Stream::Stderr, |t| t.green()),
72            self.profile.if_supports_color(Stream::Stderr, |t| t.cyan())
73        )?;
74        let docker_label = match self.docker_status.as_str() {
75            "running" => format!(
76                "{}: {}",
77                "Docker".if_supports_color(Stream::Stderr, |t| t.green()),
78                "daemon running".if_supports_color(Stream::Stderr, |t| t.green())
79            ),
80            "not_running" => format!(
81                "{}: {}",
82                "Docker".if_supports_color(Stream::Stderr, |t| t.green()),
83                "daemon not running".if_supports_color(Stream::Stderr, |t| t.yellow())
84            ),
85            _ => format!(
86                "{}: {}",
87                "Docker".if_supports_color(Stream::Stderr, |t| t.green()),
88                "not installed".if_supports_color(Stream::Stderr, |t| t.yellow())
89            ),
90        };
91        writeln!(f, "{}", docker_label)?;
92
93        match &self.remote {
94            RemoteOutput::Success {
95                environment_id,
96                server_cluster_health,
97            } => {
98                writeln!(
99                    f,
100                    "{}: {}",
101                    "Environment".if_supports_color(Stream::Stderr, |t| t.green()),
102                    environment_id.if_supports_color(Stream::Stderr, |t| t.cyan())
103                )?;
104                let cluster_line = match server_cluster_health {
105                    ServerClusterHealth::Healthy => format!(
106                        "{}: {} ({})",
107                        "Server cluster".if_supports_color(Stream::Stderr, |t| t.green()),
108                        SERVER_CLUSTER_NAME.if_supports_color(Stream::Stderr, |t| t.cyan()),
109                        "healthy"
110                    ),
111                    ServerClusterHealth::NotReady { reason } => format!(
112                        "{}: {} ({}: {})\n  hint: run `mz-deploy setup`",
113                        "Server cluster".if_supports_color(Stream::Stderr, |t| t.green()),
114                        SERVER_CLUSTER_NAME.if_supports_color(Stream::Stderr, |t| t.cyan()),
115                        "not ready".if_supports_color(Stream::Stderr, |t| t.yellow()),
116                        reason,
117                    ),
118                    ServerClusterHealth::Missing => format!(
119                        "{}: {} ({})\n  hint: run `mz-deploy setup`",
120                        "Server cluster".if_supports_color(Stream::Stderr, |t| t.green()),
121                        SERVER_CLUSTER_NAME.if_supports_color(Stream::Stderr, |t| t.cyan()),
122                        "missing".if_supports_color(Stream::Stderr, |t| t.red()),
123                    ),
124                };
125                write!(f, "{}", cluster_line)?;
126            }
127            RemoteOutput::Failure { host, port } => {
128                write!(
129                    f,
130                    "{} {}:{}",
131                    "Failed to connect to".if_supports_color(Stream::Stderr, |t| t.red()),
132                    host.if_supports_color(Stream::Stderr, |t| t.cyan()),
133                    port.to_string()
134                        .if_supports_color(Stream::Stderr, |t| t.cyan())
135                )?;
136            }
137        }
138
139        Ok(())
140    }
141}
142
143/// Test database connection with the specified profile.
144///
145/// # Arguments
146/// * `profile` - Database profile containing connection information
147///
148/// # Returns
149/// Ok(()) if connection succeeds
150///
151/// # Errors
152/// Returns `CliError::Connection` if connection fails
153pub async fn run(settings: &Settings) -> Result<(), CliError> {
154    let profile = settings.connection();
155    let docker_status = DockerRuntime::check_availability().await;
156    let docker_status_str = match docker_status {
157        DockerStatus::Running => "running",
158        DockerStatus::NotRunning => "not_running",
159        DockerStatus::NotInstalled => "not_installed",
160    };
161
162    let client = Client::connect_with_profile(profile.clone()).await;
163
164    let remote = match client {
165        Ok(client) => {
166            let (environment_id, cluster_result) =
167                tokio::join!(query_session_info(&client), check_server_cluster(&client),);
168
169            let environment_id = environment_id?;
170            let server_cluster_health = cluster_result?;
171
172            RemoteOutput::Success {
173                environment_id,
174                server_cluster_health,
175            }
176        }
177        Err(_) => RemoteOutput::Failure {
178            host: profile.require_host()?.to_string(),
179            port: profile.port,
180        },
181    };
182
183    let output = DebugOutput {
184        profile: profile.name.clone(),
185        docker_status: docker_status_str.to_string(),
186        remote,
187    };
188
189    log::output(&output);
190
191    Ok(())
192}
193
194async fn query_session_info(client: &Client) -> Result<String, CliError> {
195    let row = client
196        .query_one("SELECT mz_environment_id() AS environment_id", &[])
197        .await?;
198
199    let environment_id: String = row.get("environment_id");
200
201    Ok(environment_id)
202}