diff --git a/tools/kagami/src/main.rs b/tools/kagami/src/main.rs index 42959ce0eeb..d0acb37e61e 100644 --- a/tools/kagami/src/main.rs +++ b/tools/kagami/src/main.rs @@ -58,23 +58,7 @@ pub enum Args { Docs(Box), /// Generate the default validator Validator(validator::Args), - /// Generate a docker-compose configuration for a variable number of peers - /// using a Dockerhub image, GitHub repo, or a local Iroha repo. - /// - /// This command builds the docker-compose configuration in a specified directory. If the source - /// is a GitHub repo, it will be cloned into the directory. Also, the default configuration is - /// built and put into `/config` directory, unless `--no-default-configuration` flag is - /// provided. The default configuration is equivalent to running `kagami config peer`, - /// `kagami validator`, and `kagami genesis default --compiled-validator-path ./validator.wasm` consecutively. - /// - /// Default configuration building will fail if Kagami is run outside of Iroha repo (tracking - /// issue: https://github.com/hyperledger/iroha/issues/3473). If you are going to run it outside - /// of the repo, make sure to pass `--no-default-configuration` flag. - /// - /// Be careful with specifying a Dockerhub image as a source: Kagami Swarm only guarantees that - /// the docker-compose configuration it generates is compatible with the same Git revision it - /// is built from itself. Therefore, if specified image is not compatible with the version of Swarm - /// you are running, the generated configuration might not work. + /// Generate Docker Compose configuration Swarm(swarm::Args), } diff --git a/tools/kagami/src/swarm.rs b/tools/kagami/src/swarm.rs index dff49b4bae7..1b486a37bd9 100644 --- a/tools/kagami/src/swarm.rs +++ b/tools/kagami/src/swarm.rs @@ -18,7 +18,7 @@ use path_absolutize::Absolutize; use serialize_docker_compose::{DockerCompose, DockerComposeService, ServiceSource}; use ui::UserInterface; -use super::{ClapArgs, Outcome}; +use super::Outcome; const GIT_REVISION: &str = env!("VERGEN_GIT_SHA"); const GIT_ORIGIN: &str = "https://github.com/hyperledger/iroha.git"; @@ -31,166 +31,462 @@ const FILE_VALIDATOR: &str = "validator.wasm"; const FILE_CONFIG: &str = "config.json"; const FILE_GENESIS: &str = "genesis.json"; const FILE_COMPOSE: &str = "docker-compose.yml"; -const DIR_FORCE_SUGGESTION: &str = - "You can pass `--outdir-force` flag to remove the directory without prompting"; +const FORCE_ARG_SUGGESTION: &str = + "You can pass `--force` flag to remove the file/directory without prompting"; const GENESIS_KEYPAIR_SEED: &[u8; 7] = b"genesis"; -#[derive(ClapArgs, Debug)] -pub struct Args { - #[command(flatten)] - source: ImageSourceArgs, - /// How many peers to generate within the docker-compose. - #[arg(long, short)] - peers: NonZeroU16, - /// Target directory where to place generated files. - /// - /// If the directory is not empty, Kagami will prompt it's re-creation. If the TTY is not - /// interactive, Kagami will stop execution with non-zero exit code. In order to re-create - /// the directory anyway, pass `--outdir-force` flag. - #[arg(long)] - outdir: PathBuf, - /// Re-create the target directory if it already exists. - #[arg(long)] - outdir_force: bool, - /// Do not create default configuration in the `/config` directory. - /// - /// Default `config.json`, `genesis.json` and `validator.wasm` are generated and put into - /// the `/config` directory. That directory is specified in the `volumes` field - /// of the Docker Compose file. - /// - /// Setting this flag prevents copying of default configuration files into the output folder. The `config` directory will still be - /// created, but the necessary configuration should be put there by the user manually. - #[arg(long)] - no_default_configuration: bool, - /// Might be useful for deterministic key generation. - /// - /// It could be any string. Its UTF-8 bytes will be used as a seed. - #[arg(long, short)] - seed: Option, +mod clap_args { + use clap::{Args, Subcommand}; + + use super::*; + + #[derive(Args, Debug)] + pub struct SwarmArgs { + /// How many peers to generate within the Docker Compose setup. + #[arg(long, short)] + pub peers: NonZeroU16, + /// Used for deterministic key-generation. + /// + /// Any valid UTF-8 sequence is acceptable. + // TODO: Check for length limitations, and if non-UTF-8 sequences are working. + #[arg(long, short)] + pub seed: Option, + /// Re-create the target directory (for `dir` subcommand) or file (for `file` subcommand) + /// if they already exist. + #[arg(long)] + pub force: bool, + + #[command(subcommand)] + pub command: SwarmMode, + } + + #[derive(Subcommand, Debug)] + pub enum SwarmMode { + /// Produce a directory with Docker Compose configuration, Iroha configuration, and an option + /// to clone Iroha and use it as a source. + /// + /// This command builds Docker Compose configuration in a specified directory. If the source + /// is a GitHub repo, it will be cloned into the directory. Also, the default configuration is + /// built and put into `/config` directory, unless `--no-default-configuration` flag is + /// provided. The default configuration is equivalent to running `kagami config peer`, + /// `kagami validator`, and `kagami genesis default --compiled-validator-path ./validator.wasm` + /// consecutively. + /// + /// Default configuration building will fail if Kagami is run outside of Iroha repo (tracking + /// issue: https://github.com/hyperledger/iroha/issues/3473). If you are going to run it outside + /// of the repo, make sure to pass `--no-default-configuration` flag. + Dir { + /// Target directory where to place generated files. + /// + /// If the directory is not empty, Kagami will prompt it's re-creation. If the TTY is not + /// interactive, Kagami will stop execution with non-zero exit code. In order to re-create + /// the directory anyway, pass `--force` flag. + outdir: PathBuf, + /// Do not create default configuration in the `/config` directory. + /// + /// Default `config.json`, `genesis.json` and `validator.wasm` are generated and put into + /// the `/config` directory. That directory is specified in the `volumes` field + /// of the Docker Compose file. + /// + /// Setting this flag prevents copying of default configuration files into the output folder. + /// The `config` directory will still be created, but the necessary configuration should be put + /// there by the user manually. + #[arg(long)] + no_default_configuration: bool, + #[command(flatten)] + source: ModeDirSource, + }, + /// Produce only a single Docker Compose configuration file + File { + /// Path to a generated Docker Compose configuration. + /// + /// If file exists, Kagami will prompt its overwriting. If the TTY is not + /// interactive, Kagami will stop execution with non-zero exit code. In order to + /// overwrite the file anyway, pass `--force` flag. + outfile: PathBuf, + /// Path to a directory with Iroha configuration. It will be mapped as volume for containers. + /// + /// The directory should contain `config.json` and `genesis.json`. + #[arg(long)] + config_dir: PathBuf, + #[command(flatten)] + source: ModeFileSource, + }, + } + + #[derive(Args, Debug)] + #[group(required = true, multiple = false)] + pub struct ModeDirSource { + /// Use Iroha GitHub source as a build source + /// + /// Clone `hyperledger/iroha` repo from the revision Kagami is built itself, + /// and use the cloned source code to build images from. + #[arg(long)] + pub build_from_github: bool, + /// Use specified docker image. + /// + /// Be careful with specifying a Dockerhub image as a source: Kagami Swarm only guarantees that + /// the docker-compose configuration it generates is compatible with the same Git revision it + /// is built from itself. Therefore, if specified image is not compatible with the version of Swarm + /// you are running, the generated configuration might not work. + #[arg(long)] + pub image: Option, + /// Use local path location of the Iroha source code to build images from. + /// + /// If the path is relative, it will be resolved relative to the CWD. + #[arg(long, value_name = "PATH")] + pub build: Option, + } + + #[derive(Args, Debug)] + #[group(required = true, multiple = false)] + // FIXME: I haven't found a way how to share `image` and `build` options between `file` and + // `dir` modes with correct grouping logic. `command(flatten)` doesn't work for it, + // so it's hard to share a single struct with "base source options" + pub struct ModeFileSource { + /// Same as `--image` for `swarm dir` subcommand + #[arg(long)] + pub image: Option, + /// Same as `--build` for `swarm build` subcommand + #[arg(long, value_name = "PATH")] + pub build: Option, + } + + #[cfg(test)] + mod tests { + use std::fmt::{Debug, Display, Formatter}; + + use clap::{ArgMatches, Command, Error as ClapError}; + + use super::*; + + struct ClapErrorWrap(ClapError); + + impl From for ClapErrorWrap { + fn from(value: ClapError) -> Self { + Self(value) + } + } + + impl Debug for ClapErrorWrap { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } + } + + fn match_args(args_str: impl AsRef) -> Result { + let cmd = Command::new("test"); + let cmd = SwarmArgs::augment_args(cmd); + let matches = cmd.try_get_matches_from( + std::iter::once("test").chain(args_str.as_ref().split(' ')), + )?; + Ok(matches) + } + + #[test] + fn works_in_file_mode() { + let _ = match_args("-p 20 file --build . --config-dir ./config sample.yml").unwrap(); + } + + #[test] + fn works_in_dir_mode_with_github_source() { + let _ = match_args("-p 20 dir --build-from-github swarm").unwrap(); + } + + #[test] + fn doesnt_allow_config_dir_for_dir_mode() { + let _ = match_args("-p 1 dir --build-from-github --config-dir ./ swarm").unwrap_err(); + } + + #[test] + fn doesnt_allow_multiple_sources_in_dir_mode() { + let _ = match_args("-p 1 dir --build-from-github --build . swarm").unwrap_err(); + } + + #[test] + fn doesnt_allow_multiple_sources_in_file_mode() { + let _ = match_args("-p 1 file --build . --image hp/iroha --config-dir ./ test.yml") + .unwrap_err(); + } + + #[test] + fn doesnt_allow_github_source_in_file_mode() { + let _ = + match_args("-p 1 file --build-from-github --config-dir ./ test.yml").unwrap_err(); + } + + #[test] + fn doesnt_allow_omitting_source_in_dir_mode() { + let _ = match_args("-p 1 dir ./test").unwrap_err(); + } + + #[test] + fn doesnt_allow_omitting_source_in_file_mode() { + let _ = match_args("-p 1 file test.yml --config-dir ./").unwrap_err(); + } + } } +pub use clap_args::SwarmArgs as Args; +use clap_args::{ModeDirSource, ModeFileSource}; + impl Args { pub fn run(self) -> Outcome { - let ui = UserInterface::new(); + let parsed: ParsedArgs = self.into(); + parsed.run() + } +} - let prepare_dir_strategy = if self.outdir_force { - PrepareDirectoryStrategy::ForceRecreate - } else { - PrepareDirectoryStrategy::Prompt +/// Type-strong version of [`Args`] with no ambiguity between arguments relationships +struct ParsedArgs { + peers: NonZeroU16, + seed: Option, + /// User allowance to override existing files/directories + force: bool, + mode: ParsedMode, +} + +impl From for ParsedArgs { + fn from( + Args { + peers, + force, + seed, + command, + }: Args, + ) -> Self { + let mode: ParsedMode = match command { + clap_args::SwarmMode::File { + outfile, + config_dir, + source, + } => ParsedMode::File { + target_file: outfile, + config_dir, + image_source: source.into(), + }, + clap_args::SwarmMode::Dir { + outdir, + no_default_configuration, + source, + } => ParsedMode::Directory { + target_dir: outdir, + no_default_configuration, + image_source: source.into(), + }, }; - let source = ImageSource::from(self.source); - let target_dir = TargetDirectory::new(AbsolutePath::absolutize(self.outdir)?); - if let EarlyEnding::Halt = target_dir - .prepare(&prepare_dir_strategy, &ui) - .wrap_err("Failed to prepare directory")? - { - return Ok(()); + Self { + peers, + force, + seed, + mode, } + } +} - let config_dir = AbsolutePath::absolutize(target_dir.path.join(DIR_CONFIG))?; +impl ParsedArgs { + pub fn run(self) -> Outcome { + let ui = UserInterface::new(); - let source = source - .resolve(&target_dir, &ui) - .wrap_err("Failed to resolve the source of image")?; + let Self { + peers, + seed, + force, + mode, + } = self; + let seed = seed.map(String::into_bytes); + let seed = seed.as_deref(); + + match mode { + ParsedMode::Directory { + target_dir, + no_default_configuration, + image_source, + } => { + let target_file_raw = target_dir.join(FILE_COMPOSE); + let target_dir = TargetDirectory::new(AbsolutePath::absolutize(&target_dir)?); + let config_dir = AbsolutePath::absolutize(&target_dir.path.join(DIR_CONFIG))?; + let target_file = AbsolutePath::absolutize(&target_file_raw)?; + + let prepare_dir_strategy = if force { + PrepareDirectoryStrategy::ForceRecreate + } else { + PrepareDirectoryStrategy::Prompt + }; - let ui = if self.no_default_configuration { - PrepareConfigurationStrategy::GenerateOnlyDirectory - } else { - PrepareConfigurationStrategy::GenerateDefault - } - .run(&config_dir, ui) - .wrap_err("Failed to prepare configuration")?; - - DockerComposeBuilder { - target_dir: target_dir.path.clone(), - config_dir, - source, - peers: self.peers, - seed: self.seed.map(String::into_bytes), - } - .build() - .wrap_err("Failed to build docker compose")? - .write_file(&target_dir.path.join(FILE_COMPOSE)) - .wrap_err("Failed to write compose file")?; + if let EarlyEnding::Halt = target_dir + .prepare(&prepare_dir_strategy, &ui) + .wrap_err("Failed to prepare directory")? + { + return Ok(()); + } + + let image_source = image_source + .resolve(&target_dir, &ui) + .wrap_err("Failed to resolve the source of image")?; + + let ui = if no_default_configuration { + PrepareConfigurationStrategy::GenerateOnlyDirectory + } else { + PrepareConfigurationStrategy::GenerateDefault + } + .run(&config_dir, ui) + .wrap_err("Failed to prepare configuration")?; + + DockerComposeBuilder { + target_file: &target_file, + config_dir: &config_dir, + image_source, + peers, + seed, + } + .build_and_write()?; + + ui.log_directory_mode_complete(&target_dir.path, &target_file_raw); - ui.log_complete(&target_dir.path); + Ok(()) + } + ParsedMode::File { + target_file, + config_dir, + image_source, + } => { + let target_file_raw = target_file; + let target_file = AbsolutePath::absolutize(&target_file_raw)?; + let config_dir = AbsolutePath::absolutize(&config_dir)?; + + if target_file.exists() && !force { + if let ui::PromptAnswer::No = ui.prompt_remove_target_file(&target_file)? { + return Ok(()); + } + } - Ok(()) + let image_source = image_source + .resolve() + .wrap_err("Failed to resolve the source of image")?; + + DockerComposeBuilder { + target_file: &target_file, + config_dir: &config_dir, + image_source, + peers, + seed, + } + .build_and_write()?; + + ui.log_file_mode_complete(&target_file, &target_file_raw); + + Ok(()) + } + } } } -#[derive(ClapArgs, Clone, Debug)] -#[group(required = true, multiple = false)] -struct ImageSourceArgs { - /// Use specified docker image. - #[arg(long)] - image: Option, - /// Use local path location of the Iroha source code to build images from. - /// - /// If the path is relative, it will be resolved relative to the CWD. - #[arg(long, value_name = "PATH")] - build: Option, - /// Clone `hyperledger/iroha` repo from the revision Kagami is built itself, - /// and use the cloned source code to build images from. - #[arg(long)] - build_from_github: bool, +enum ParsedMode { + Directory { + target_dir: PathBuf, + no_default_configuration: bool, + image_source: SourceForDirectory, + }, + File { + target_file: PathBuf, + config_dir: PathBuf, + image_source: SourceForFile, + }, } -/// Parsed version of [`ImageSourceArgs`] -#[derive(Clone, Debug)] -enum ImageSource { - Image { name: String }, - GitHub { revision: String }, - Path(PathBuf), +enum SourceForDirectory { + SameAsForFile(SourceForFile), + BuildFromGitHub, } -impl From for ImageSource { - fn from(args: ImageSourceArgs) -> Self { - match args { - ImageSourceArgs { - image: Some(name), .. - } => Self::Image { name }, - ImageSourceArgs { +impl From for SourceForDirectory { + fn from(value: ModeDirSource) -> Self { + match value { + ModeDirSource { + build: Some(path), + image: None, + build_from_github: false, + } => Self::SameAsForFile(SourceForFile::Build { path }), + ModeDirSource { + build: None, + image: Some(name), + build_from_github: false, + } => Self::SameAsForFile(SourceForFile::Image { name }), + ModeDirSource { + build: None, + image: None, build_from_github: true, - .. - } => Self::GitHub { - revision: GIT_REVISION.to_owned(), - }, - ImageSourceArgs { - build: Some(path), .. - } => Self::Path(path), - _ => unreachable!("Clap must ensure the invariant"), + } => Self::BuildFromGitHub, + _ => unreachable!("clap invariant"), } } } -impl ImageSource { - /// Has a side effect: if self is [`Self::GitHub`], it clones the repo into +impl SourceForDirectory { + /// Has a side effect: if self is [`Self::BuildFromGitHub`], it clones the repo into /// the target directory. fn resolve(self, target: &TargetDirectory, ui: &UserInterface) -> Result { - let source = match self { - Self::Path(path) => ResolvedImageSource::Build { - path: AbsolutePath::absolutize(path).wrap_err("Failed to resolve build path")?, - }, - Self::GitHub { revision } => { + match self { + Self::SameAsForFile(source_for_file) => source_for_file.resolve(), + Self::BuildFromGitHub => { let clone_dir = target.path.join(DIR_CLONE); - let clone_dir = AbsolutePath::absolutize(clone_dir)?; + let clone_dir = AbsolutePath::absolutize(&clone_dir)?; ui.log_cloning_repo(); - shallow_git_clone(GIT_ORIGIN, revision, &clone_dir) + shallow_git_clone(GIT_ORIGIN, GIT_REVISION, &clone_dir) .wrap_err("Failed to clone the repo")?; - ResolvedImageSource::Build { path: clone_dir } + Ok(ResolvedImageSource::Build { path: clone_dir }) } + } + } +} + +enum SourceForFile { + Image { name: String }, + Build { path: PathBuf }, +} + +impl From for SourceForFile { + fn from(value: ModeFileSource) -> Self { + match value { + ModeFileSource { + image: Some(name), + build: None, + } => Self::Image { name }, + ModeFileSource { + image: None, + build: Some(path), + } => Self::Build { path }, + _ => unreachable!("clap invariant"), + } + } +} + +impl SourceForFile { + fn resolve(self) -> Result { + let resolved = match self { Self::Image { name } => ResolvedImageSource::Image { name }, + Self::Build { path: relative } => { + let absolute = + AbsolutePath::absolutize(&relative).wrap_err("Failed to resolve build path")?; + ResolvedImageSource::Build { path: absolute } + } }; - Ok(source) + Ok(resolved) } } +#[derive(Debug)] +enum ResolvedImageSource { + Image { name: String }, + Build { path: AbsolutePath }, +} + fn shallow_git_clone( remote: impl AsRef, revision: impl AsRef, @@ -220,12 +516,6 @@ fn shallow_git_clone( Ok(()) } -#[derive(Debug)] -enum ResolvedImageSource { - Image { name: String }, - Build { path: AbsolutePath }, -} - enum PrepareConfigurationStrategy { GenerateDefault, GenerateOnlyDirectory, @@ -370,31 +660,39 @@ impl TargetDirectory { } #[derive(Debug)] -struct DockerComposeBuilder { - target_dir: AbsolutePath, - config_dir: AbsolutePath, - source: ResolvedImageSource, +struct DockerComposeBuilder<'a> { + /// Needed to compute a relative source build path + target_file: &'a AbsolutePath, + /// Needed to put into `volumes` + config_dir: &'a AbsolutePath, + image_source: ResolvedImageSource, peers: NonZeroU16, - seed: Option>, + /// Crypto seed to use for keys generation + seed: Option<&'a [u8]>, } -impl DockerComposeBuilder { +impl DockerComposeBuilder<'_> { fn build(&self) -> Result { - let base_seed = self.seed.as_deref(); + let target_file_dir = self.target_file.parent().ok_or_else(|| { + eyre!( + "Cannot get a directory of a file {}", + self.target_file.display() + ) + })?; - let peers = peer_generator::generate_peers(self.peers, base_seed) + let peers = peer_generator::generate_peers(self.peers, self.seed) .wrap_err("Failed to generate peers")?; - let genesis_key_pair = generate_key_pair(base_seed, GENESIS_KEYPAIR_SEED) + let genesis_key_pair = generate_key_pair(self.seed, GENESIS_KEYPAIR_SEED) .wrap_err("Failed to generate genesis key pair")?; - let service_source = match &self.source { + let service_source = match &self.image_source { ResolvedImageSource::Build { path } => { - ServiceSource::Build(path.relative_to(&self.target_dir)?) + ServiceSource::Build(path.relative_to(target_file_dir)?) } ResolvedImageSource::Image { name } => ServiceSource::Image(name.clone()), }; let volumes = vec![( self.config_dir - .relative_to(&self.target_dir)? + .relative_to(target_file_dir)? .to_str() .wrap_err("Config directory path is not a valid string")? .to_owned(), @@ -437,6 +735,14 @@ impl DockerComposeBuilder { let compose = DockerCompose::new(services); Ok(compose) } + + fn build_and_write(&self) -> Result<()> { + let target_file = self.target_file; + let compose = self + .build() + .wrap_err("Failed to build a docker compose file")?; + compose.write_file(&target_file.path) + } } #[derive(Clone, Debug)] @@ -465,10 +771,10 @@ impl AsRef for AbsolutePath { } impl AbsolutePath { - fn absolutize(path: PathBuf) -> Result { + fn absolutize(path: &PathBuf) -> Result { Ok(Self { path: if path.is_absolute() { - path + path.clone() } else { path.absolutize()?.to_path_buf() }, @@ -476,12 +782,12 @@ impl AbsolutePath { } /// Relative path from self to other. - fn relative_to(&self, other: &AbsolutePath) -> Result { + fn relative_to(&self, other: &(impl AsRef + ?Sized)) -> Result { pathdiff::diff_paths(self, other) .ok_or_else(|| { eyre!( "failed to build relative path from {} to {}", - other.display(), + other.as_ref().display(), self.display(), ) }) @@ -586,7 +892,7 @@ mod serialize_docker_compose { use iroha_primitives::addr::SocketAddr; use serde::{ser::Error as _, Serialize, Serializer}; - use crate::swarm::peer_generator::Peer; + use super::peer_generator::Peer; const COMMAND_SUBMIT_GENESIS: &str = "iroha --submit-genesis"; const DOCKER_COMPOSE_VERSION: &str = "3.8"; @@ -608,9 +914,9 @@ mod serialize_docker_compose { pub fn write_file(&self, path: &PathBuf) -> Result<(), color_eyre::Report> { let yaml = serde_yaml::to_string(self).wrap_err("Failed to serialise YAML")?; File::create(path) - .wrap_err_with(|| eyre!("Failed to create file: {:?}", path))? + .wrap_err_with(|| eyre!("Failed to create file {}", path.display()))? .write_all(yaml.as_bytes()) - .wrap_err("Failed to write YAML content")?; + .wrap_err_with(|| eyre!("Failed to write YAML content into {}", path.display()))?; Ok(()) } } @@ -1018,11 +1324,12 @@ mod serialize_docker_compose { } mod ui { + use std::path::Path; + use color_eyre::Help; use owo_colors::OwoColorize; - use super::{AbsolutePath, Result}; - use crate::swarm::DIR_FORCE_SUGGESTION; + use super::{AbsolutePath, Result, FORCE_ARG_SUGGESTION}; mod prefix { use owo_colors::{FgColorDisplay, OwoColorize}; @@ -1047,6 +1354,16 @@ mod ui { No, } + impl From for PromptAnswer { + fn from(value: bool) -> Self { + if value { + Self::Yes + } else { + Self::No + } + } + } + #[derive(Copy, Clone)] pub(super) enum TargetDirectoryAction { Created, @@ -1105,14 +1422,23 @@ mod ui { )) .with_default(false) .prompt() - .suggestion(DIR_FORCE_SUGGESTION) - .map(|flag| { - if flag { - PromptAnswer::Yes - } else { - PromptAnswer::No - } - }) + .suggestion(FORCE_ARG_SUGGESTION) + .map(PromptAnswer::from) + } + + #[allow(clippy::unused_self)] + pub(super) fn prompt_remove_target_file( + &self, + file: &AbsolutePath, + ) -> Result { + inquire::Confirm::new(&format!( + "File {} already exists. Remove it?", + file.display().blue().bold() + )) + .with_default(false) + .prompt() + .suggestion(FORCE_ARG_SUGGESTION) + .map(PromptAnswer::from) } #[allow(clippy::unused_self)] @@ -1125,13 +1451,28 @@ mod ui { } #[allow(clippy::unused_self)] - pub(super) fn log_complete(&self, dir: &AbsolutePath) { + pub(super) fn log_directory_mode_complete(&self, dir: &AbsolutePath, file_raw: &Path) { println!( "{} Docker compose configuration is ready at:\n\n {}\ - \n\n You could `{}` in it.", + \n\n You could run `{} {} {}`", prefix::success(), dir.display().green().bold(), - "docker compose up".blue() + "docker compose -f".blue(), + file_raw.display().blue().bold(), + "up".blue(), + ); + } + + #[allow(clippy::unused_self)] + pub(super) fn log_file_mode_complete(&self, file: &AbsolutePath, file_raw: &Path) { + println!( + "{} Docker compose configuration is ready at:\n\n {}\ + \n\n You could run `{} {} {}`", + prefix::success(), + file.display().green().bold(), + "docker compose -f".blue(), + file_raw.display().blue().bold(), + "up".blue(), ); } } @@ -1209,16 +1550,20 @@ mod tests { #[test] fn generate_peers_deterministically() { let root = Path::new("/"); - let seed: Vec<_> = b"iroha".to_vec(); + let seed = Some(b"iroha".to_vec()); + let seed = seed.as_deref(); let composed = DockerComposeBuilder { - target_dir: AbsolutePath::from_virtual(&PathBuf::from("/test"), root), - config_dir: AbsolutePath::from_virtual(&PathBuf::from("/test/config"), root), + target_file: &AbsolutePath::from_virtual( + &PathBuf::from("/test/docker-compose.yml"), + root, + ), + config_dir: &AbsolutePath::from_virtual(&PathBuf::from("/test/config"), root), peers: 4.try_into().unwrap(), - source: ResolvedImageSource::Build { + image_source: ResolvedImageSource::Build { path: AbsolutePath::from_virtual(&PathBuf::from("/test/iroha-cloned"), root), }, - seed: Some(seed), + seed, } .build() .expect("should build with no errors");