Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clap-v4 support for hab cli #9330

Merged
merged 7 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

245 changes: 245 additions & 0 deletions components/common/src/cli/clap_validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,248 @@ impl clap_v4::builder::TypedValueParser for HabPackageInstallSourceValueParser {
}
}
}

/// Struct implementing validator for Habitat Origin
///
/// Validates with `habitat_core::origin::Origin::validate` function.
#[derive(Clone)]
pub struct HabOriginValueParser;

impl clap_v4::builder::TypedValueParser for HabOriginValueParser {
type Value = String;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
let val = value.to_str().unwrap().to_string();

let result = habitat_core::origin::Origin::validate(val);
if result.is_err() {
let mut err =
clap_v4::Error::new(clap_v4::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(clap_v4::error::ContextKind::InvalidArg,
clap_v4::error::ContextValue::String(arg.to_string()));
}
err.insert(clap_v4::error::ContextKind::InvalidValue,
clap_v4::error::ContextValue::String(format!("`{}`: {}",
value.to_string_lossy(),
result.err().unwrap(),)));
Err(err)
} else {
Ok(value.to_str().unwrap().to_string())
}
}
}

/// Struct implimenting validator that validates the value is a valid path
#[derive(Clone)]
pub struct FileExistsValueParser;

impl clap_v4::builder::TypedValueParser for FileExistsValueParser {
type Value = String;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
parse_ref_internal(cmd, arg, value, false, false, "is not a valid file")
}
}

// TODO: This will be used by `hab config` (this implements the functionality of
// `file_exists_or_stdin` validator in Clap v2.
/// Struct implementing validator that validates the valie is a valid 'file' or 'stdin'
#[derive(Clone)]
pub struct FileExistsOrStdinValueParser;
agadgil-progress marked this conversation as resolved.
Show resolved Hide resolved

impl clap_v4::builder::TypedValueParser for FileExistsOrStdinValueParser {
type Value = String;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
parse_ref_internal(cmd, arg, value, false, true, "is not a valid file or stdin")
}
}

/// Struct implemeitng validator that valudates the value is a valid directory
///
/// Internally uses `ValidPathValueParser`
#[derive(Clone)]
pub struct DirExistsValueParser;

impl clap_v4::builder::TypedValueParser for DirExistsValueParser {
type Value = std::path::PathBuf;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
parse_ref_internal(cmd, arg, value, true, false, "is not a valid directory").map(|x| {
x.into()
})
}
}

// Checks whether a give path is a file or a dir or stdin, used internally by the validators
//
// eg. FileExistsValueParser will call this command with both `check_dir` and `check_stdin` set to
// false. DirExistsValueParser will call this command with `check_dir` set to `true` and
// `check_stdin` set to `false` etc.
fn check_valid_file_dir_stdin(path: &std::path::Path, check_dir: bool, check_stdin: bool) -> bool {
let mut is_valid = path.is_file();

if !is_valid && check_dir {
is_valid = path.is_dir();
}

is_valid = if check_stdin {
if is_valid {
is_valid
} else if let Some(v) = path.to_str() {
v == "-"
} else {
false
}
} else {
is_valid
};

is_valid
}

fn parse_ref_internal(cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr,
check_dir: bool,
check_stdin: bool,
err_str: &str)
-> Result<String, clap_v4::Error> {
let val = value.to_str().unwrap().to_string();

let result = std::path::Path::new(&val);
if !check_valid_file_dir_stdin(result, check_dir, check_stdin) {
let mut err = clap_v4::Error::new(clap_v4::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(clap_v4::error::ContextKind::InvalidArg,
clap_v4::error::ContextValue::String(arg.to_string()));
}
err.insert(clap_v4::error::ContextKind::InvalidValue,
clap_v4::error::ContextValue::String(format!("`{}`: {}",
value.to_string_lossy(),
err_str,)));
Err(err)
} else {
Ok(value.to_str().unwrap().to_string())
}
}
/// Validate a given file is a 'toml' file or contains valid package idents only.
///
/// Packages to be installed can be read from a 'toml' file or a file containing package idents
/// only. The actual validation of whether the contents of the 'toml' file are correct is performed
/// by the actual command that calls this validation. This validation will succeed if a file is a
/// 'toml' file (possibly in wrong format) or the file contains packaged identifiers, one per line.
#[derive(Clone)]
pub struct TomlOrPkgIdentFileValueParser;

use crate::cli::{file_into_idents,
is_toml_file};

impl clap_v4::builder::TypedValueParser for TomlOrPkgIdentFileValueParser {
type Value = String;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
let val = value.to_str().unwrap().to_string();

if is_toml_file(&val) {
return Ok(val);
}

let result = file_into_idents(&val);
if result.is_err() {
let mut err =
clap_v4::Error::new(clap_v4::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(clap_v4::error::ContextKind::InvalidArg,
clap_v4::error::ContextValue::String(arg.to_string()));
}
err.insert(clap_v4::error::ContextKind::InvalidValue,
clap_v4::error::ContextValue::String(format!("`{}`: {}",
value.to_string_lossy(),
result.err().unwrap(),)));
Err(err)
} else {
Ok(val)
}
}
}

/// Validates whether given input is a valid Package Identifier
///
/// This validator returns success if the given input is a valid simple Package Identifier or a
/// fully qualified PackageIdentifier
///
/// Use `value_parser = HabPkgIdentValueParser::simple()` for simple Package Identifier.
/// Use `value_parser = HabPkgIdentValueParser::full()` for fully qualified Package Identifier.
#[derive(Clone)]
pub struct HabPkgIdentValueParser {
agadgil-progress marked this conversation as resolved.
Show resolved Hide resolved
fully_qualified: bool,
}

impl HabPkgIdentValueParser {
/// For Simple Package Identifier of the form 'origin/name'
pub fn simple() -> Self { Self { fully_qualified: false, } }

/// For Full Package Identifier of the form 'origin/name/version/release'
pub fn full() -> Self { Self { fully_qualified: true, } }
}

use habitat_core::package::ident::{FullyQualifiedPackageIdent,
PackageIdent};

impl clap_v4::builder::TypedValueParser for HabPkgIdentValueParser {
type Value = PackageIdent;

fn parse_ref(&self,
cmd: &clap_v4::Command,
arg: Option<&clap_v4::Arg>,
value: &std::ffi::OsStr)
-> Result<Self::Value, clap_v4::Error> {
let val = value.to_str().unwrap().to_string();

let result = if self.fully_qualified {
FullyQualifiedPackageIdent::from_str(&val).err()
} else {
PackageIdent::from_str(&val).err()
};

if result.is_some() {
let mut err =
clap_v4::Error::new(clap_v4::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(clap_v4::error::ContextKind::InvalidArg,
clap_v4::error::ContextValue::String(arg.to_string()));
}
err.insert(clap_v4::error::ContextKind::InvalidValue,
clap_v4::error::ContextValue::String(format!("`{}`: {}",
value.to_string_lossy(),
result.unwrap(),)));
Err(err)
} else {
Ok(val.into())
}
}
}

// TODO: Add Unit tests for all validators
6 changes: 6 additions & 0 deletions components/core/src/package/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ impl FromStr for PackageIdent {
}
}

impl From<String> for PackageIdent {
fn from(ident: String) -> Self {
Self::from_str(ident.as_str()).expect("Invalid Package Identifier")
}
}

impl PartialOrd for PackageIdent {
/// Packages can be compared according to the following:
///
Expand Down
14 changes: 10 additions & 4 deletions components/hab/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ doc = false
base64 = "*"
bitflags = "*"
chrono = {version = "*", features = ["serde"]}
clap = { git = "https://github.com/habitat-sh/clap.git", branch = "v2-master", features = [ "suggestions", "color", "unstable" ] }
configopt = { git = "https://github.com/habitat-sh/configopt.git" }
ctrlc = "*"
dirs = "*"
env_logger = "*"
Expand All @@ -42,7 +40,6 @@ same-file = "*"
serde = { version = "*", features = ["derive"] }
serde_json = { version = "*", features = [ "preserve_order" ] }
serde_yaml = "*"
structopt = { git = "https://github.com/habitat-sh/structopt.git" }
tabwriter = "*"
tar = "*"
termcolor = "*"
Expand All @@ -55,6 +52,13 @@ walkdir = "*"
rustls-webpki = { version = "*", features = ["alloc"] }
tempfile = "*"


clap = { git = "https://github.com/habitat-sh/clap.git", branch = "v2-master", features = [ "suggestions", "color", "unstable" ] , optional = true}
configopt = { git = "https://github.com/habitat-sh/configopt.git" , optional = true}
structopt = { git = "https://github.com/habitat-sh/structopt.git" , optional = true}

clap_v4 = { version = "4", package = "clap", features = ["env", "derive", "string", "wrap_help"], optional = true }

[dependencies.uuid]
version = "*"
features = ["v4"]
Expand All @@ -65,6 +69,8 @@ winapi = { version = "^0.3", features = ["winuser", "windef"] }
winreg = "*"

[features]
default = ["supported_targets"]
v2 = [ "clap", "configopt", "structopt" ]
v4 = [ "supported_targets", "clap_v4" ]
default = ["supported_targets", "v2"]
functional = []
supported_targets = ["habitat_core/supported_targets"]
47 changes: 4 additions & 43 deletions components/hab/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ use habitat_common::{cli::{file_into_idents,
use habitat_core::{origin::Origin as CoreOrigin,
package::{Identifiable,
PackageIdent}};
use serde::{Deserialize,
Serialize};
use std::{fmt,
path::Path,
use std::{path::Path,
result,
str::FromStr};
use structopt::StructOpt;
Expand All @@ -23,45 +20,9 @@ use structopt::StructOpt;
/// ran to completion with a successful result. The Launcher should not attempt to restart
/// the Supervisor and should exit immediately with a successful exit code.
pub const OK_NO_RETRY_EXCODE: i32 = 84;
pub const AFTER_HELP: &str =
"\nALIASES:\n apply Alias for: 'config apply'\n install Alias for: 'pkg \
install'\n run Alias for: 'sup run'\n setup Alias for: 'cli setup'\n \
start Alias for: 'svc start'\n stop Alias for: 'svc stop'\n term \
Alias for: 'sup term'\n";

pub fn get(_feature_flags: FeatureFlag) -> App<'static, 'static> { Hab::clap() }

////////////////////////////////////////////////////////////////////////

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum KeyType {
Public,
Secret,
}

impl FromStr for KeyType {
type Err = crate::error::Error;

fn from_str(value: &str) -> result::Result<Self, Self::Err> {
match value {
"public" => Ok(Self::Public),
"secret" => Ok(Self::Secret),
_ => Err(Self::Err::KeyTypeParseError(value.to_string())),
}
}
}

impl fmt::Display for KeyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyType::Public => write!(f, "public"),
KeyType::Secret => write!(f, "secret"),
}
}
}

////////////////////////////////////////////////////////////////////////

pub fn parse_optional_arg<T: FromStr>(name: &str, m: &ArgMatches) -> Option<T>
where <T as std::str::FromStr>::Err: std::fmt::Debug
{
Expand Down Expand Up @@ -147,20 +108,20 @@ mod tests {
"sup",
"run",
"--application",
"--environment=env"]);
"--environment=env",]);
assert!(r.is_ok());
let r = get(no_feature_flags()).get_matches_from_safe(vec!["hab",
"svc",
"load",
"--application=app",
"--environment",
"pkg/ident"]);
"pkg/ident",]);
assert!(r.is_ok());
let r = get(no_feature_flags()).get_matches_from_safe(vec!["hab",
"svc",
"load",
"--application",
"pkg/ident"]);
"pkg/ident",]);
assert!(r.is_ok());
}

Expand Down
Loading
Loading