Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- Allow adding custom CLI arguments to `run` subcommand ([#291]).

### Changed
- BREAKING: clap 2.33.3 -> 3.0.4 ([#289]).
- BREAKING: `cli::Command::Run` now just wraps `cli::ProductOperatorRun` rather than defining the struct inline ([#291]).

[#289]: https://github.com/stackabletech/operator-rs/pull/289
[#291]: https://github.com/stackabletech/operator-rs/pull/291

## [0.7.0] - 2021-12-22


Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repository = "https://github.com/stackabletech/operator-rs"
[dependencies]
async-trait = "0.1.51"
chrono = "0.4.19"
clap = "2.33.3"
clap = { version = "3.0.4", features = ["derive", "cargo"] }
const_format = "0.2.22"
either = "1.6.1"
futures = "0.3.17"
Expand All @@ -35,7 +35,6 @@ tracing-subscriber = { version = "0.3.1", features = ["env-filter"] }
uuid = { version = "0.8.2", features = ["v4"] }
backoff = "0.4.0"
derivative = "2.2.0"
structopt = "0.3.25"

[dev-dependencies]
rstest = "0.12.0"
Expand Down
183 changes: 121 additions & 62 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
//! This module provides helper methods to deal with common CLI options using the `clap` crate.
//!
//! In particular it currently supports handling two kinds of options:
//! * CRD handling (printing & saving to a file)
//! * CRD printing
//! * Product config location
//!
//! # Example
//!
//! This example show the usage of the CRD functionality.
//!
//! ```
//! ```no_run
//! // Handle CLI arguments
//! use clap::{crate_version, SubCommand};
//! use clap::App;
//! use clap::{crate_version, App, Parser};
//! use kube::{CustomResource, CustomResourceExt};
//! use schemars::JsonSchema;
//! use serde::{Deserialize, Serialize};
//! use stackable_operator::cli;
//! use stackable_operator::error::OperatorResult;
//! use kube::CustomResource;
//! use schemars::JsonSchema;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(Clone, CustomResource, Debug, JsonSchema, Serialize, Deserialize)]
//! #[kube(
Expand All @@ -40,25 +39,30 @@
//! pub name: String,
//! }
//!
//! #[derive(clap::Parser)]
//! #[clap(
//! name = "Foobar Operator",
//! author,
//! version,
//! about = "Stackable Operator for Foobar"
//! )]
//! struct Opts {
//! #[clap(subcommand)]
//! command: cli::Command,
//! }
//!
//! # fn main() -> OperatorResult<()> {
//! let matches = App::new("Spark Operator")
//! .author("Stackable GmbH - info@stackable.de")
//! .about("Stackable Operator for Foobar")
//! .version(crate_version!())
//! .subcommand(
//! SubCommand::with_name("crd")
//! .subcommand(cli::generate_crd_subcommand::<FooCluster>())
//! .subcommand(cli::generate_crd_subcommand::<BarCluster>())
//! )
//! .get_matches();
//! let opts = Opts::from_args();
//!
//! if let ("crd", Some(subcommand)) = matches.subcommand() {
//! if cli::handle_crd_subcommand::<FooCluster>(subcommand)? {
//! return Ok(());
//! };
//! if cli::handle_crd_subcommand::<BarCluster>(subcommand)? {
//! return Ok(());
//! };
//! match opts.command {
//! cli::Command::Crd => println!(
//! "{}{}",
//! serde_yaml::to_string(&FooCluster::crd())?,
//! serde_yaml::to_string(&BarCluster::crd())?,
//! ),
//! cli::Command::Run { .. } => {
//! // Run the operator
//! }
//! }
//! # Ok(())
//! # }
Expand All @@ -67,77 +71,132 @@
//! Product config handling works similarly:
//!
//! ```no_run
//! use clap::{crate_version, SubCommand};
//! use clap::{crate_version, App, Parser};
//! use stackable_operator::cli;
//! use stackable_operator::error::OperatorResult;
//! use clap::App;
//!
//! #[derive(clap::Parser)]
//! #[clap(
//! name = "Foobar Operator",
//! author,
//! version,
//! about = "Stackable Operator for Foobar"
//! )]
//! struct Opts {
//! #[clap(subcommand)]
//! command: cli::Command,
//! }
//!
//! # fn main() -> OperatorResult<()> {
//! let matches = App::new("Spark Operator")
//! .author("Stackable GmbH - info@stackable.de")
//! .about("Stackable Operator for Foobar")
//! .version(crate_version!())
//! .arg(cli::generate_productconfig_arg())
//! .get_matches();
//! let opts = Opts::from_args();
//!
//! let paths = vec![
//! "deploy/config-spec/properties.yaml",
//! "/etc/stackable/spark-operator/config-spec/properties.yaml",
//! ];
//! let product_config_path = cli::handle_productconfig_arg(&matches, paths)?;
//! match opts.command {
//! cli::Command::Crd => {
//! // Print CRD objects
//! }
//! cli::Command::Run(cli::ProductOperatorRun { product_config }) => {
//! let product_config = product_config.load(&[
//! "deploy/config-spec/properties.yaml",
//! "/etc/stackable/spark-operator/config-spec/properties.yaml",
//! ])?;
//! }
//! }
//! # Ok(())
//! # }
//!
//! ```
//!
//!
use crate::error;
use crate::error::OperatorResult;
#[allow(deprecated)]
use crate::CustomResourceExt;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use clap::{App, AppSettings, Arg, ArgMatches, Args};
use product_config::ProductConfigManager;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use structopt::StructOpt;

pub const AUTHOR: &str = "Stackable GmbH - info@stackable.de";

/// Framework-standardized commands
///
/// If you need operator-specific commands then you can flatten [`Command`] into your own command enum. For example:
/// ```rust
/// #[derive(structopt::StructOpt)]
/// #[derive(clap::Parser)]
/// enum Command {
/// /// Print hello world message
/// Hello,
/// #[structopt(flatten)]
/// #[clap(flatten)]
/// Framework(stackable_operator::cli::Command)
/// }
/// ```
#[derive(StructOpt)]
#[derive(clap::Parser, Debug, PartialEq, Eq)]
// The enum-level doccomment is intended for developers, not end users
// so supress it from being included in --help
#[structopt(long_about = "")]
pub enum Command {
#[clap(long_about = "")]
pub enum Command<Run: Args = ProductOperatorRun> {
/// Print CRD objects
Crd,
/// Run operator
Run {
/// Provides the path to a product-config file
#[structopt(
long,
short = "p",
value_name = "FILE",
default_value = "",
parse(from_os_str)
)]
product_config: ProductConfigPath,
},
Run(Run),
}

/// Default parameters that all product operators take when running
///
/// Can be embedded into an extended argument set:
///
/// ```rust
/// # use stackable_operator::cli::{Command, ProductOperatorRun, ProductConfigPath};
/// #[derive(clap::Parser, Debug, PartialEq, Eq)]
/// struct Run {
/// #[clap(long)]
/// name: String,
/// #[clap(flatten)]
/// common: ProductOperatorRun,
/// }
/// use clap::Parser;
/// let opts = Command::<Run>::parse_from(["foobar-operator", "run", "--name", "foo", "--product-config", "bar"]);
/// assert_eq!(opts, Command::Run(Run {
/// name: "foo".to_string(),
/// common: ProductOperatorRun {
/// product_config: ProductConfigPath::from("bar".as_ref()),
/// },
/// }));
/// ```
///
/// or replaced entirely
///
/// ```rust
/// # use stackable_operator::cli::{Command, ProductOperatorRun};
/// #[derive(clap::Parser, Debug, PartialEq, Eq)]
/// struct Run {
/// #[clap(long)]
/// name: String,
/// }
/// use clap::Parser;
/// let opts = Command::<Run>::parse_from(["foobar-operator", "run", "--name", "foo"]);
/// assert_eq!(opts, Command::Run(Run {
/// name: "foo".to_string(),
/// }));
/// ```
#[derive(clap::Parser, Debug, PartialEq, Eq)]
#[clap(long_about = "")]
pub struct ProductOperatorRun {
/// Provides the path to a product-config file
#[clap(
long,
short = 'p',
value_name = "FILE",
default_value = "",
parse(from_os_str)
)]
pub product_config: ProductConfigPath,
}

/// A path to a [`ProductConfigManager`] spec file
#[derive(Debug, PartialEq, Eq)]
pub struct ProductConfigPath {
path: Option<PathBuf>,
}
Expand Down Expand Up @@ -200,9 +259,9 @@ const PRODUCT_CONFIG_ARG: &str = "product-config";
///
/// See the module level documentation for a complete example.
#[deprecated(note = "use ProductConfigPath (or Command) instead")]
pub fn generate_productconfig_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(PRODUCT_CONFIG_ARG)
.short("p")
pub fn generate_productconfig_arg() -> Arg<'static> {
Arg::new(PRODUCT_CONFIG_ARG)
.short('p')
.long(PRODUCT_CONFIG_ARG)
.value_name("FILE")
.help("Provides the path to a product-config file")
Expand Down Expand Up @@ -248,23 +307,23 @@ pub fn handle_productconfig_arg(
/// returns: App
#[deprecated(note = "use Command instead")]
#[allow(deprecated)]
pub fn generate_crd_subcommand<'a, 'b, T>() -> App<'a, 'b>
pub fn generate_crd_subcommand<T>() -> App<'static>
where
T: CustomResourceExt,
{
let kind = T::api_resource().kind;

SubCommand::with_name(&kind.to_lowercase())
App::new(&kind.to_lowercase())
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("print")
.short("p")
.short('p')
.long("print")
.help("Will print the CRD schema in YAML format to stdout"),
)
.arg(
Arg::with_name("save")
.short("s")
.short('s')
.long("save")
.takes_value(true)
.value_name("FILE")
Expand Down