diff --git a/Cargo.lock b/Cargo.lock index cb62a1618..12c28dfe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3794,9 +3794,15 @@ dependencies = [ name = "openstack-cli-config" version = "0.13.8" dependencies = [ + "clap", + "eyre", "indexmap", + "openstack-cli-core", "serde", + "serde_json", + "structable", "thiserror 2.0.20", + "tracing", "yaml_serde", "yamlpatch", "yamlpath", @@ -4602,6 +4608,7 @@ dependencies = [ "openstack-cli-block-storage", "openstack-cli-catalog", "openstack-cli-compute", + "openstack-cli-config", "openstack-cli-container-infrastructure-management", "openstack-cli-core", "openstack-cli-dns", diff --git a/cli/config/Cargo.toml b/cli/config/Cargo.toml index 108ee050a..ffb1e59ac 100644 --- a/cli/config/Cargo.toml +++ b/cli/config/Cargo.toml @@ -17,9 +17,15 @@ homepage.workspace = true repository.workspace = true [dependencies] +clap.workspace = true +eyre.workspace = true indexmap.workspace = true +openstack-cli-core.workspace = true serde.workspace = true +serde_json.workspace = true +structable = { workspace = true } thiserror.workspace = true +tracing.workspace = true yaml_serde.workspace = true yamlpatch.workspace = true yamlpath.workspace = true diff --git a/cli/config/src/lib.rs b/cli/config/src/lib.rs index 14177362d..162c8de56 100644 --- a/cli/config/src/lib.rs +++ b/cli/config/src/lib.rs @@ -13,9 +13,41 @@ // SPDX-License-Identifier: Apache-2.0 //! Local client configuration file operations. //! -//! This crate is the foundation for `osc config` commands that read and -//! edit `clouds.yaml`/`secure.yaml` in place. It currently provides -//! [`yaml_edit`], a comment- and anchor-preserving YAML editor; command -//! implementations built on top of it (e.g. `clouds add`) land separately. +//! This crate is the foundation for `osc config` commands. It currently +//! provides: +//! +//! * [`show`], displaying the effective local CLI configuration +//! (`$XDG_CONFIG_HOME/osc/config.yaml`). +//! * [`yaml_edit`], a comment- and anchor-preserving YAML editor for +//! `clouds.yaml`/`secure.yaml`; command implementations built on top of +//! it (e.g. `clouds add`) land separately. + +use clap::{Parser, Subcommand}; + +use openstack_cli_core::{cli::CliArgs, error::OpenStackCliError}; +pub mod show; pub mod yaml_edit; + +/// Local `osc` client configuration. +#[derive(Debug, Parser)] +pub struct ConfigCommand { + /// Config management commands + #[command(subcommand)] + pub command: ConfigCommands, +} + +#[allow(missing_docs)] +#[derive(Debug, Subcommand)] +pub enum ConfigCommands { + Show(show::ShowCommand), +} + +impl ConfigCommand { + /// Perform command action. + pub async fn take_action(&self, parsed_args: &C) -> Result<(), OpenStackCliError> { + match &self.command { + ConfigCommands::Show(cmd) => cmd.take_action(parsed_args).await, + } + } +} diff --git a/cli/config/src/show.rs b/cli/config/src/show.rs new file mode 100644 index 000000000..8b12e2837 --- /dev/null +++ b/cli/config/src/show.rs @@ -0,0 +1,84 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Show the effective local `osc` CLI configuration. + +use clap::Parser; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::info; + +use openstack_cli_core::config::Config; +use openstack_cli_core::output::{OutputFor, OutputProcessor}; +use openstack_cli_core::{cli::CliArgs, error::OpenStackCliError}; +use structable::{StructTable, StructTableOptions}; + +/// Show the effective local CLI configuration. +/// +/// This is the `$XDG_CONFIG_HOME/osc/config.yaml` configuration merged with +/// the built-in defaults. It controls CLI-only behavior (output views, +/// hints) and is unrelated to `clouds.yaml`/`secure.yaml` cloud connection +/// credentials. +#[derive(Debug, Parser)] +pub struct ShowCommand {} + +/// A displayable view of the effective CLI configuration. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, StructTable)] +pub struct ConfigView { + /// Configured output views, keyed by resource. + #[structable(serialize)] + pub views: Value, + /// Configured per-resource command hints. + #[structable(serialize)] + pub command_hints: Value, + /// General hints shown independent of the command. + #[structable(serialize)] + pub hints: Value, + /// Whether hints are shown after a successful command. + #[structable()] + pub enable_hints: bool, +} + +impl TryFrom<&Config> for ConfigView { + type Error = eyre::Report; + fn try_from(value: &Config) -> Result { + Ok(Self { + views: serde_json::to_value(&value.views)?, + command_hints: serde_json::to_value(&value.command_hints)?, + hints: serde_json::to_value(&value.hints)?, + enable_hints: value.enable_hints, + }) + } +} + +impl ShowCommand { + /// Perform command action. + pub async fn take_action(&self, parsed_args: &C) -> Result<(), OpenStackCliError> { + info!("Show effective CLI configuration"); + + let op = OutputProcessor::from_args(parsed_args, Some("config"), Some("show")); + let config = parsed_args.config(); + + match op.target { + OutputFor::Human => { + op.output_human(&ConfigView::try_from(config)?)?; + } + _ => { + op.output_machine(serde_json::to_value(config)?)?; + } + } + op.show_command_hint()?; + Ok(()) + } +} diff --git a/cli/core/src/config.rs b/cli/core/src/config.rs index ffb5fc2ca..0fe68bf37 100644 --- a/cli/core/src/config.rs +++ b/cli/core/src/config.rs @@ -30,7 +30,7 @@ //! ``` use eyre::Result; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fmt, @@ -107,7 +107,7 @@ pub enum ConfigBuilderError { /// Output configuration /// /// This structure is controlling how the table table is being built for a structure. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ViewConfig { /// Limit fields (their titles) to be returned #[serde(default)] @@ -121,7 +121,7 @@ pub struct ViewConfig { } /// Field output configuration -#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialOrd, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialOrd, PartialEq, Serialize)] pub struct FieldConfig { /// Attribute name pub name: String, @@ -150,7 +150,7 @@ const fn _default_true() -> bool { } /// OpenStackClient configuration -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Config { /// Map of views with the key being the resource key `.[/]`) /// and the value being an `[OutputConfig]` diff --git a/openstack_cli/Cargo.toml b/openstack_cli/Cargo.toml index 837347861..f5c91d0ba 100644 --- a/openstack_cli/Cargo.toml +++ b/openstack_cli/Cargo.toml @@ -69,6 +69,7 @@ openstack-cli-auth.workspace = true openstack-cli-block-storage.workspace = true openstack-cli-catalog.workspace = true openstack-cli-compute.workspace = true +openstack-cli-config.workspace = true openstack-cli-container-infrastructure-management.workspace = true openstack-cli-core.workspace = true openstack-cli-dns.workspace = true diff --git a/openstack_cli/src/cli.rs b/openstack_cli/src/cli.rs index f1f9a0f23..4f0a8aea0 100644 --- a/openstack_cli/src/cli.rs +++ b/openstack_cli/src/cli.rs @@ -119,6 +119,7 @@ pub enum TopLevelCommands { BlockStorage(openstack_cli_block_storage::BlockStorageCommand), Catalog(openstack_cli_catalog::CatalogCommand), Compute(openstack_cli_compute::ComputeCommand), + Config(openstack_cli_config::ConfigCommand), #[command(aliases = ["container-infrastructure-management", "container"])] ContainerInfrastructure( openstack_cli_container_infrastructure_management::ContainerInfrastructureCommand, @@ -152,6 +153,10 @@ impl Cli { TopLevelCommands::BlockStorage(args) => args.take_action(self, client).await, TopLevelCommands::Catalog(args) => args.take_action(self, client).await, TopLevelCommands::Compute(args) => args.take_action(self, client).await, + // `osc config` never needs a cloud connection and is dispatched + // before this point in `entry_point`; kept here only to make + // this match exhaustive. + TopLevelCommands::Config(_) => unimplemented!(), TopLevelCommands::ContainerInfrastructure(args) => args.take_action(self, client).await, TopLevelCommands::Dns(args) => args.take_action(self, client).await, TopLevelCommands::Identity(args) => args.take_action(self, client).await, diff --git a/openstack_cli/src/lib.rs b/openstack_cli/src/lib.rs index deba557cc..a9ea22474 100644 --- a/openstack_cli/src/lib.rs +++ b/openstack_cli/src/lib.rs @@ -77,6 +77,12 @@ pub async fn entry_point() -> Result<(), OpenStackCliError> { return args.take_action(&cli).await; } + if let TopLevelCommands::Config(args) = &cli.command { + // `osc config` commands edit/read local configuration files and + // must not trigger cloud authentication. + return args.take_action(&cli).await; + } + if let TopLevelCommands::Auth(args) = &cli.command && let Some(cmd) = args.as_offline_cache_clear_all() {