Skip to content
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Add destination prefixes to replica create, update, and show commands, [PR-247](https://github.com/reductstore/reduct-cli/pull/247) by @ychampion

### Changed

- Pin the repo Rust toolchain to `1.91.0`, require locked Cargo resolution in CI/pre-commit/install examples/publish flows, and replace the tag-version `cargo-get` install step with repo-local `cargo metadata` verification.
- Align replica commands with API v1.21 by removing the deprecated `--each-s` option.
Comment thread
ychampion marked this conversation as resolved.

## 0.12.2 - 2026-06-18

Expand Down
8 changes: 3 additions & 5 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ documentation = "https://reduct.store/docs/guides"
description = "A CLI client for ReductStore written in Rust"

[dependencies]
reduct-rs = { version = "1.20.0"}
reduct-rs = { version = "1.20.0", git = "https://github.com/reductstore/reduct-rs", branch = "main" }
clap = { version = "4.6.1", features = ["cargo"] }
dirs = "6.0.0"
toml = "1.0.6"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ blob data.

## Features

* Support for ReductStore API v1.20
* Support for ReductStore API v1.21
* Easy management of buckets, tokens, replications and lifecycle policies
* Ability to check the status of a storage engine
* Aliases for storing server credentials
Expand Down
11 changes: 10 additions & 1 deletion src/cmd/replica.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ use crate::cmd::replica::mode::{
use crate::cmd::replica::rm::{rm_replica_cmd, rm_replica_handler};
use crate::cmd::replica::show::{show_replica_cmd, show_replica_handler};
use crate::cmd::replica::update::{update_replica_cmd, update_replica_handler};
use clap::Command;
use clap::{Arg, Command};
use create::{create_replica, create_replica_cmd};

fn make_prefix_arg() -> Arg {
Arg::new("prefix")
.long("prefix")
.value_name("PREFIX")
.help("Prefix added to destination entry names")
.required(false)
}

pub(crate) fn replication_cmd() -> Command {
Command::new("replica")
.about("Manage replication tasks")
Expand Down Expand Up @@ -75,6 +83,7 @@ mod tests {
.dst_host("http://localhost:8383")
.src_bucket(bucket)
.dst_bucket(bucket2)
.dst_prefix("robot-1")
.send()
.await?;

Expand Down
61 changes: 54 additions & 7 deletions src/cmd/replica/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::cmd::replica::make_prefix_arg;
use crate::cmd::RESOURCE_PATH_HELP;
use crate::io::reduct::{build_client, parse_url_and_token};
use crate::parse::widely_used_args::{make_each_n, make_each_s, make_entries_arg, make_when_arg};
use crate::parse::widely_used_args::{make_each_n, make_entries_arg, make_when_arg};
use crate::parse::{Resource, ResourcePathParser};
use clap::{Arg, Command};
use reduct_rs::ReplicationSettings;
Expand Down Expand Up @@ -33,7 +34,7 @@ pub(super) fn create_replica_cmd() -> Command {
)
.arg(make_entries_arg())
.arg(make_each_n())
.arg(make_each_s())
.arg(make_prefix_arg())
.arg(make_when_arg())
}

Expand All @@ -59,7 +60,7 @@ pub(super) async fn create_replica(
.collect::<Vec<String>>();

let each_n = args.get_one::<u64>("each-n");
let each_s = args.get_one::<f64>("each-s");
let prefix = args.get_one::<String>("prefix");
let when = args.get_one::<String>("when");

let client = build_client(ctx, &alias_or_url).await?;
Expand All @@ -72,7 +73,7 @@ pub(super) async fn create_replica(
settings.dst_token = Some(token);
settings.entries = entries_filter;
settings.each_n = each_n.copied();
settings.each_s = each_s.copied();
settings.dst_prefix = prefix.cloned().unwrap_or_default();

if let Some(when) = when {
settings.when = Some(serde_json::from_str(&when)?);
Expand Down Expand Up @@ -119,8 +120,8 @@ mod tests {
"entry2",
"--each-n",
"10",
"--each-s",
"0.5",
"--prefix",
"robot-1",
"--when",
r#"{"&label": {"$gt": 10}}"#,
]);
Expand All @@ -136,7 +137,7 @@ mod tests {
"Keep compatibility with v1.16"
);
assert_eq!(replica.settings.each_n, Some(10));
assert_eq!(replica.settings.each_s, Some(0.5));
assert_eq!(replica.settings.dst_prefix, "robot-1");
assert_eq!(
replica.settings.when.unwrap(),
json!({"&label": {"$gt": 10}})
Expand All @@ -154,4 +155,50 @@ mod tests {
]);
assert!(args.is_err());
}

#[rstest]
#[tokio::test]
async fn test_create_replica_without_prefix(
context: crate::context::CliContext,
#[future] replica: String,
#[future] bucket: String,
#[future] bucket2: String,
) {
let test_replica = replica.await;
let bucket = bucket.await;
let bucket2 = bucket2.await;

let client = build_client(&context, "local").await.unwrap();
client.create_bucket(&bucket).send().await.unwrap();
client.create_bucket(&bucket2).send().await.unwrap();

let args = create_replica_cmd()
.try_get_matches_from([
"create".to_string(),
format!("local/{test_replica}"),
bucket,
format!("local/{bucket2}"),
])
.unwrap();
create_replica(&context, &args).await.unwrap();

let replica = client.get_replication(&test_replica).await.unwrap();
assert!(replica.settings.dst_prefix.is_empty());
}

#[test]
fn test_create_replica_with_prefix() {
let args = create_replica_cmd()
.try_get_matches_from([
"create",
"local/test_replica",
"source",
"local/destination",
"--prefix",
"robot-1",
])
.unwrap();

assert_eq!(args.get_one::<String>("prefix").unwrap(), "robot-1");
}
}
9 changes: 9 additions & 0 deletions src/cmd/replica/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ pub(super) async fn show_replica_handler(
labeled_cell("Source Bucket", replica.settings.src_bucket.clone()),
labeled_cell("Destination Bucket", replica.settings.dst_bucket.clone()),
labeled_cell("Destination Server", replica.settings.dst_host.clone()),
labeled_cell(
"Destination Prefix",
if replica.settings.dst_prefix.is_empty() {
"None".to_string()
} else {
replica.settings.dst_prefix.clone()
},
),
labeled_cell("Entries", format!("{:?}", replica.settings.entries)),
];

Expand Down Expand Up @@ -154,6 +162,7 @@ mod tests {
labeled_cell("Source Bucket", "test_bucket"),
labeled_cell("Destination Bucket", "test_bucket_2"),
labeled_cell("Destination Server", "http://localhost:8383"),
labeled_cell("Destination Prefix", "robot-1"),
labeled_cell("Entries", "[]"),
"When: None".to_string(),
]);
Expand Down
71 changes: 62 additions & 9 deletions src/cmd/replica/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::cmd::replica::make_prefix_arg;
use crate::cmd::RESOURCE_PATH_HELP;
use crate::context::CliContext;
use crate::io::reduct::{build_client, parse_url_and_token};
use crate::parse::widely_used_args::{make_each_n, make_each_s, make_entries_arg, make_when_arg};
use crate::parse::widely_used_args::{make_each_n, make_entries_arg, make_when_arg};
use crate::parse::{Resource, ResourcePathParser};

use clap::{Arg, ArgMatches, Command};
Expand Down Expand Up @@ -37,7 +38,7 @@ pub(super) fn update_replica_cmd() -> Command {
)
.arg(make_entries_arg())
.arg(make_each_n())
.arg(make_each_s())
.arg(make_prefix_arg())
.arg(make_when_arg())
}

Expand Down Expand Up @@ -80,7 +81,7 @@ fn update_replication_settings(
.map(|s| s.map(|s| s.to_string()).collect::<Vec<String>>());

let each_n = args.get_one::<u64>("each-n");
let each_s = args.get_one::<f64>("each-s");
let prefix = args.get_one::<String>("prefix");
let when = args.get_one::<String>("when");

let (dest_url, token) = parse_url_and_token(ctx, &dest_alias_or_url)?;
Expand All @@ -100,8 +101,8 @@ fn update_replication_settings(
current_settings.each_n = Some(*each_n);
}

if let Some(each_s) = each_s {
current_settings.each_s = Some(*each_s);
if let Some(prefix) = prefix {
current_settings.dst_prefix = prefix.clone();
}

if let Some(when) = when {
Expand Down Expand Up @@ -142,8 +143,8 @@ mod tests {
format!("local/{}", &bucket2).as_str(),
"--each-n",
"10",
"--each-s",
"0.5",
"--prefix",
"robot-2",
])
.unwrap();

Expand All @@ -160,7 +161,22 @@ mod tests {
);
assert!(replica.settings.entries.is_empty());
assert_eq!(replica.settings.each_n, Some(10));
assert_eq!(replica.settings.each_s, Some(0.5));
assert_eq!(replica.settings.dst_prefix, "robot-2");
}

#[test]
fn test_update_replica_with_prefix() {
let args = update_replica_cmd()
.try_get_matches_from([
"update",
"local/test_replica",
"local/destination",
"--prefix",
"robot-2",
])
.unwrap();

assert_eq!(args.get_one::<String>("prefix").unwrap(), "robot-2");
}

mod update_settings {
Expand Down Expand Up @@ -260,17 +276,54 @@ mod tests {
);
}

#[rstest]
fn override_prefix(context: CliContext, current_settings: ReplicationSettings) {
let args = update_replica_cmd()
.try_get_matches_from([
"update",
"local/test_replica",
"local/bucket2",
"--prefix",
"robot-2",
])
.unwrap();

let new_settings =
update_replication_settings(&context, &args, current_settings.clone()).unwrap();
assert_eq!(
new_settings,
ReplicationSettings {
dst_prefix: "robot-2".to_string(),
..current_settings
}
);
}

#[rstest]
fn preserve_prefix_when_omitted(
context: CliContext,
current_settings: ReplicationSettings,
) {
let args = update_replica_cmd()
.try_get_matches_from(["update", "local/test_replica", "local/bucket2"])
.unwrap();

let new_settings =
update_replication_settings(&context, &args, current_settings).unwrap();
assert_eq!(new_settings.dst_prefix, "robot-1");
}

#[fixture]
fn current_settings(current_token: String) -> ReplicationSettings {
ReplicationSettings {
src_bucket: "bucket1".to_string(),
dst_bucket: "bucket2".to_string(),
dst_host: "http://localhost:8383/".to_string(),
dst_token: Some(current_token),
dst_prefix: "robot-1".to_string(),
include: Labels::from_iter(vec![("key1".to_string(), "value1".to_string())]),
exclude: Labels::from_iter(vec![("key2".to_string(), "value2".to_string())]),
each_n: None,
each_s: None,
entries: vec!["entry1".to_string(), "entry2".to_string()],
when: None,
mode: Default::default(),
Expand Down
Loading