Skip to main content

mz_deploy/cli/commands/
profile.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//! `mz-deploy profile {list,set,current}` — manage the project's default profile.
11//!
12//! Modeled on `kubectl config` (contexts). The default profile is recorded
13//! per-project and per-developer, so team members can each pick their own
14//! default without touching shared configuration. Resolution order for any
15//! command: `--profile` flag, then `MZ_DEPLOY_PROFILE`, then the recorded
16//! project default, then error.
17//!
18//! Subcommands:
19//!
20//! - [`list`] — every profile defined in `profiles.toml`, with the currently
21//!   resolved profile marked `(active)`.
22//! - [`set`] — records `<name>` as the project default after validating that
23//!   the profile exists in `profiles.toml`.
24//! - [`current`] — prints the resolved profile and where it came from (flag,
25//!   env var, or project default), or reports that no profile has been
26//!   selected.
27
28use crate::cli::CliError;
29use crate::config::{ProfilesConfig, read_mzprofile, write_mzprofile};
30use crate::{info, log};
31use owo_colors::{OwoColorize, Stream, Style};
32use serde::Serialize;
33use std::fmt;
34use std::path::Path;
35
36/// Renderable result for `profile list`.
37#[derive(Serialize)]
38struct ProfileListing {
39    profiles: Vec<ProfileEntry>,
40
41    source: String,
42}
43
44#[derive(Serialize)]
45struct ProfileEntry {
46    name: String,
47    active: bool,
48}
49
50impl fmt::Display for ProfileListing {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        if self.profiles.is_empty() {
53            return write!(f, "No profiles found in {}", self.source);
54        }
55        let mut first = true;
56        for entry in &self.profiles {
57            if !first {
58                writeln!(f)?;
59            }
60            first = false;
61            if entry.active {
62                write!(
63                    f,
64                    "  {}  {}",
65                    entry.name.if_supports_color(Stream::Stderr, |t| t.green()),
66                    "(active)".if_supports_color(Stream::Stderr, |t| t.dimmed())
67                )?;
68            } else {
69                write!(f, "  {}", entry.name)?;
70            }
71        }
72        Ok(())
73    }
74}
75
76/// List every profile defined in `profiles.toml` and mark the active one.
77pub fn list(
78    directory: &Path,
79    cli_profile: Option<&str>,
80    profiles_dir: Option<&Path>,
81) -> Result<(), CliError> {
82    let profiles_config = ProfilesConfig::load(profiles_dir)?;
83    let active = resolve_active(directory, cli_profile)?;
84    let names = profiles_config.profile_names();
85
86    let profiles = names
87        .iter()
88        .map(|name| ProfileEntry {
89            name: (*name).to_string(),
90            active: active.as_deref() == Some(*name),
91        })
92        .collect();
93
94    log::output(&ProfileListing {
95        profiles,
96        source: profiles_config.source_path().display().to_string(),
97    });
98    Ok(())
99}
100
101/// Record `name` as the project default.
102///
103/// Validates that the profile exists in `profiles.toml` so typos fail at
104/// `set` time rather than at the next command invocation.
105pub fn set(directory: &Path, profiles_dir: Option<&Path>, name: &str) -> Result<(), CliError> {
106    let profiles_config = ProfilesConfig::load(profiles_dir)?;
107    // `get_profile` returns ConfigError::ProfileNotFound if missing.
108    let _ = profiles_config.get_profile(name)?;
109
110    write_mzprofile(directory, name)?;
111
112    let check_style = Style::new().green().bold();
113    info!(
114        "  {} default profile set to {}",
115        "✓".if_supports_color(Stream::Stderr, |t| check_style.style(t)),
116        name.if_supports_color(Stream::Stderr, |t| t.green()),
117    );
118    Ok(())
119}
120
121/// Print the resolved profile and the source it came from.
122pub fn current(directory: &Path, cli_profile: Option<&str>) -> Result<(), CliError> {
123    if let Some(name) = cli_profile {
124        // Clap surfaces `--profile` and `MZ_DEPLOY_PROFILE` through the same
125        // `cli_profile` handle; we can't distinguish which one was set without
126        // querying the env directly.
127        let source = if std::env::var_os("MZ_DEPLOY_PROFILE").is_some() {
128            "MZ_DEPLOY_PROFILE env var"
129        } else {
130            "--profile flag"
131        };
132        info!(
133            "  {} ({})",
134            name.if_supports_color(Stream::Stderr, |t| t.green()),
135            source.if_supports_color(Stream::Stderr, |t| t.dimmed())
136        );
137        return Ok(());
138    }
139
140    match read_mzprofile(directory)? {
141        Some(name) => {
142            info!(
143                "  {} ({})",
144                name.if_supports_color(Stream::Stderr, |t| t.green()),
145                "project default".if_supports_color(Stream::Stderr, |t| t.dimmed()),
146            );
147        }
148        None => {
149            info!(
150                "  {} no profile selected — run {} to set one",
151                "⚠".if_supports_color(Stream::Stderr, |t| t.yellow()),
152                "mz-deploy profile set <name>".if_supports_color(Stream::Stderr, |t| t.cyan()),
153            );
154        }
155    }
156    Ok(())
157}
158
159fn resolve_active(directory: &Path, cli_profile: Option<&str>) -> Result<Option<String>, CliError> {
160    if let Some(p) = cli_profile {
161        return Ok(Some(p.to_string()));
162    }
163    Ok(read_mzprofile(directory)?)
164}