openstack_cli_core/cli.rs
1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14//! CLI top level command and processing
15//!
16use clap::builder::{
17 Styles,
18 styling::{AnsiColor, Effects},
19};
20use clap::{Args, Parser, ValueEnum, ValueHint};
21use clap_complete::Shell;
22
23use crate::error::OpenStackCliError;
24
25use crate::config::{Config, ConfigError};
26
27pub fn styles() -> Styles {
28 Styles::styled()
29 .header(AnsiColor::Green.on_default() | Effects::BOLD)
30 .usage(AnsiColor::Green.on_default() | Effects::BOLD)
31 .literal(AnsiColor::White.on_default() | Effects::BOLD)
32 .placeholder(AnsiColor::Cyan.on_default())
33}
34
35/// Trait for getting Command execution context.
36pub trait CliArgs {
37 fn global_opts(&self) -> &GlobalOpts;
38 fn config(&self) -> &Config;
39}
40
41/// Parse config file
42pub fn parse_config(s: &str) -> Result<Config, OpenStackCliError> {
43 let mut builder = Config::builder()?;
44 if !s.is_empty() {
45 builder = builder.add_source(s).map_err(ConfigError::builder)?;
46 }
47 Ok(builder.build()?)
48}
49
50/// Connection options.
51#[derive(Args)]
52#[command(next_display_order = 800, next_help_heading = "Connection options")]
53pub struct ConnectionOpts {
54 /// Name reference to the clouds.yaml entry for the cloud configuration.
55 #[arg(long, env = "OS_CLOUD", global = true, display_order = 801, conflicts_with_all(["cloud_config_from_env", "os_cloud_name"]))]
56 pub os_cloud: Option<String>,
57
58 /// Get the cloud config from environment variables.
59 ///
60 /// Conflicts with the `--os-cloud` option. No merging of environment variables with the
61 /// options from the `clouds.yaml` file done. It is possible to rely on the `--auth-helper-cmd`
62 /// command, but than the `--os-cloud-name` should be specified to give a reasonable connection
63 /// name.
64 #[arg(long, global = true, action = clap::ArgAction::SetTrue, display_order = 802)]
65 pub cloud_config_from_env: bool,
66
67 /// Cloud name used when configuration is retrieved from environment variables. When not
68 /// specified the `envvars` would be used as a default. This value will be used eventually by
69 /// the authentication helper when data need to be provided dynamically.
70 #[arg(long, env = "OS_CLOUD_NAME", global = true, display_order = 802)]
71 pub os_cloud_name: Option<String>,
72
73 /// Project ID to use instead of the one in connection profile.
74 #[arg(long, env = "OS_PROJECT_ID", global = true, display_order = 803)]
75 pub os_project_id: Option<String>,
76
77 /// Project Name to use instead of the one in the connection profile.
78 #[arg(long, env = "OS_PROJECT_NAME", global = true, display_order = 803)]
79 pub os_project_name: Option<String>,
80
81 /// Region Name to use instead of the one in the connection profile.
82 #[arg(long, env = "OS_REGION_NAME", global = true, display_order = 804)]
83 pub os_region_name: Option<String>,
84
85 /// Custom path to the `clouds.yaml` config file.
86 #[arg(
87 long,
88 env = "OS_CLIENT_CONFIG_FILE",
89 global = true,
90 value_hint = ValueHint::FilePath,
91 display_order = 805
92 )]
93 pub os_client_config_file: Option<String>,
94
95 /// Custom path to the `secure.yaml` config file.
96 #[arg(
97 long,
98 env = "OS_CLIENT_SECURE_FILE",
99 global = true,
100 value_hint = ValueHint::FilePath,
101 display_order = 805
102 )]
103 pub os_client_secure_file: Option<String>,
104
105 /// External authentication helper command.
106 ///
107 /// Invoke external command to obtain necessary connection parameters. This is a path to the
108 /// executable, which is called with first parameter being the attribute key (i.e. `password`)
109 /// and a second parameter a cloud name (whatever is used in `--os-cloud` or
110 /// `--os-cloud-name`).
111 #[arg(long, global = true, value_hint = ValueHint::ExecutablePath, display_order = 810)]
112 pub auth_helper_cmd: Option<String>,
113
114 /// Microversion negotiation strategy used for every service.
115 ///
116 /// `floor` (default) requests the lowest microversion compatible with both the command and
117 /// the cloud, matching this tool's historical behavior. `ceiling` requests the highest
118 /// compatible microversion instead, which typically yields more complete data in responses
119 /// (e.g. commands that only started returning a field in a later microversion).
120 #[arg(
121 long,
122 env = "OS_MICROVERSION_STRATEGY",
123 global = true,
124 value_enum,
125 default_value_t = MicroVersionStrategyArg::Floor,
126 display_order = 806
127 )]
128 pub os_microversion_strategy: MicroVersionStrategyArg,
129}
130
131/// Microversion negotiation strategy (CLI-facing mirror of
132/// `openstack_sdk::MicroVersionStrategy`, kept separate so `cli/core` does not need a hard
133/// dependency on the full SDK crate for a single enum).
134#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
135pub enum MicroVersionStrategyArg {
136 /// Request the lowest microversion compatible with both the command and the cloud.
137 #[default]
138 Floor,
139 /// Request the highest microversion compatible with both the command and the cloud.
140 Ceiling,
141}
142
143/// Output configuration.
144#[derive(Args)]
145#[command(next_display_order = 900, next_help_heading = "Output options")]
146pub struct OutputOpts {
147 /// Output format.
148 #[arg(short, long, global = true, value_enum, display_order = 910)]
149 pub output: Option<OutputFormat>,
150
151 /// Fields to return in the output (only in normal and wide mode).
152 #[arg(short, long, global=true, action=clap::ArgAction::Append, display_order = 910)]
153 pub fields: Vec<String>,
154
155 /// Pretty print the output.
156 #[arg(short, long, global=true, action = clap::ArgAction::SetTrue, display_order = 910)]
157 pub pretty: bool,
158
159 /// Verbosity level. Repeat to increase level.
160 #[arg(short, long, global=true, action = clap::ArgAction::Count, display_order = 920)]
161 pub verbose: u8,
162
163 /// Output Table arrangement.
164 #[arg(long, global=true, default_value_t = TableArrangement::Dynamic, value_enum, display_order = 930)]
165 pub table_arrangement: TableArrangement,
166
167 /// Record HTTP request timings.
168 #[arg(long, global=true, action = clap::ArgAction::SetTrue, display_order = 950)]
169 pub timing: bool,
170}
171
172/// Global CLI options.
173#[derive(Args)]
174#[command(next_display_order = 900)]
175pub struct GlobalOpts {
176 /// Connection options.
177 #[command(flatten)]
178 pub connection: ConnectionOpts,
179
180 /// Output config.
181 #[command(flatten)]
182 pub output: OutputOpts,
183}
184
185/// Resolve a per-service API version selector by scanning the real process
186/// arguments directly. This has to happen before clap builds the `Command`
187/// tree (inside `Args::augment_args`), i.e. before any `ArgMatches` exist —
188/// so it can't go through clap itself. Once the tree is built, the actual
189/// (clap-parsed, env-aware, default-applying) value is read normally from
190/// `ArgMatches` in `FromArgMatches::from_arg_matches`; this function is only
191/// ever used to decide *which* version's subcommand tree to attach.
192pub fn resolve_api_version(long_flag: &str, env_var: &str, default: &str) -> String {
193 let needle_eq = format!("--{long_flag}=");
194 let needle = format!("--{long_flag}");
195 let mut result = None;
196 let mut args = std::env::args();
197 while let Some(a) = args.next() {
198 if let Some(v) = a.strip_prefix(&needle_eq) {
199 result = Some(v.to_string());
200 } else if a == needle
201 && let Some(v) = args.next()
202 {
203 result = Some(v);
204 }
205 }
206 result
207 .or_else(|| std::env::var(env_var).ok())
208 .unwrap_or_else(|| default.to_string())
209}
210
211/// Output format.
212#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
213pub enum OutputFormat {
214 /// Json output.
215 Json,
216 /// Wide (Human readable table with extra attributes). Note: this has
217 /// effect only in list operations.
218 Wide,
219}
220
221/// Table arrangement.
222#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
223pub enum TableArrangement {
224 /// Dynamically determine the width of columns in regard to terminal width and content length.
225 /// With this mode, the content in cells will wrap dynamically to get the the best column
226 /// layout for the given content.
227 #[default]
228 Dynamic,
229 /// This is mode is the same as the `Dynamic` arrangement, but it will always use as much space
230 /// as it’s given. Any surplus space will be distributed between all columns.
231 DynamicFullWidth,
232 /// Don’t do any content arrangement. Tables with this mode might become wider than your output
233 /// and look ugly.
234 Disabled,
235}
236
237/// Output shell completion code for the specified shell (bash, zsh, fish, or powershell). The
238/// shell code must be evaluated to provide interactive completion of `osc` commands. This can
239/// be done by sourcing it from the .bash_profile.
240///
241/// Examples:
242///
243/// Enable completion at a shell start:
244///
245/// `echo 'source <(osc completion bash)' >>~/.bashrc`
246///
247#[derive(Parser, Debug)]
248pub struct CompletionCommand {
249 /// If provided, outputs the completion file for given shell.
250 #[arg(default_value_t = Shell::Bash)]
251 pub shell: Shell,
252}
253
254/// Pre-connect behavior a parsed command wants from the CLI entry point.
255///
256/// Centralizes the "does this command need a live authenticated connection,
257/// and should the token be force-renewed" decision so the entry point does
258/// not need to hand-inspect the parsed command tree for every case (see
259/// `osc auth login --renew` and `osc auth status`).
260#[derive(Debug, Clone, Copy, Default)]
261pub struct ConnectionRequirements {
262 /// Whether the command requires a live, authenticated connection to the cloud.
263 ///
264 /// Commands that only read local/cached state (e.g. `osc auth status`) can set
265 /// this to `false`.
266 pub needs_auth: bool,
267 /// Whether the auth token should be force-renewed before the command runs.
268 pub renew: bool,
269}
270
271impl ConnectionRequirements {
272 /// Default requirements for a normal, connected command: needs a live
273 /// authenticated session, no forced renewal.
274 pub fn connected() -> Self {
275 Self {
276 needs_auth: true,
277 renew: false,
278 }
279 }
280}
281
282/// Provides pre-connect [`ConnectionRequirements`] for a parsed command.
283///
284/// This is implemented by hand for the top-level `Cli`/`TopLevelCommands` dispatch
285/// and the `auth` subcommand (both hand-written, not codegenerated — see
286/// `grep -r TopLevelCommands`). Should more commands need pre-connect behavior
287/// (e.g. a hypothetical purely-local `osc api-version`), a template-level
288/// `connection_requirements()` hook in the code generator would be the long-term
289/// fix; for now the mechanism is only wired where it is needed.
290pub trait ConnectionRequirementsProvider {
291 /// Return the connection requirements for this parsed command.
292 fn connection_requirements(&self) -> ConnectionRequirements {
293 ConnectionRequirements::connected()
294 }
295}