krabka_restore/args/
validate.rs1use {
11 super::{PartitionRef, RestoreArgs},
12 crate::error::RestoreError,
13 std::collections::HashSet,
14};
15
16impl RestoreArgs {
17 pub fn validate(&self) -> Result<(), RestoreError> {
26 self.archive
27 .location
28 .validate()
29 .map_err(|error| RestoreError::InvalidArgument(error.to_string()))?;
30
31 if self.archive.worm_key_id.len() != self.archive.worm_public_key.len() {
32 return Err(RestoreError::InvalidArgument(format!(
33 "--worm-key-id was given {} time(s) and --worm-public-key {} time(s); they pair by position",
34 self.archive.worm_key_id.len(),
35 self.archive.worm_public_key.len()
36 )));
37 }
38 if !self.worm_expect_head.is_empty() && self.archive.worm_key_id.is_empty() {
39 return Err(RestoreError::InvalidArgument(
40 "--worm-expect-head requires --worm-key-id and --worm-public-key".to_owned(),
41 ));
42 }
43 let mut expected_partitions = HashSet::new();
44 for expected in &self.worm_expect_head {
45 let Some((partition, head)) = expected.split_once('=') else {
46 return Err(RestoreError::InvalidArgument(format!(
47 "--worm-expect-head must be PARTITION_DIR=64_HEX, got `{expected}`"
48 )));
49 };
50 if partition.is_empty()
51 || head.len() != 64
52 || !head.bytes().all(|byte| byte.is_ascii_hexdigit())
53 {
54 return Err(RestoreError::InvalidArgument(format!(
55 "--worm-expect-head must be PARTITION_DIR=64_HEX, got `{expected}`"
56 )));
57 }
58 if !expected_partitions.insert(partition) {
59 return Err(RestoreError::InvalidArgument(format!(
60 "--worm-expect-head names `{partition}` more than once"
61 )));
62 }
63 }
64
65 let mut bounded: Vec<&PartitionRef> = Vec::with_capacity(self.to_offset.len());
66 for bound in &self.to_offset {
67 if bounded.contains(&&bound.partition) {
68 return Err(RestoreError::InvalidArgument(format!(
69 "--to-offset names {} more than once",
70 bound.partition
71 )));
72 }
73 bounded.push(&bound.partition);
74 }
75
76 for partition in super::offset_partitions(self) {
77 if !self.selects_topic(&partition.topic) {
78 return Err(RestoreError::InvalidArgument(format!(
79 "a bound names {partition}, which --topic does not select"
80 )));
81 }
82 }
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use {
90 crate::{Cli, args::test_support::args_from, error::RestoreError},
91 assert2::check,
92 clap::Parser as _,
93 };
94
95 #[yare::parameterized(
96 s3_region = { &["--archive-s3-region", "eu-west-1"][..] },
97 s3_endpoint = { &["--archive-s3-endpoint", "http://minio:9000"][..] },
98 s3_access_key = { &["--archive-s3-access-key-id", "key"][..] },
99 s3_secret_key = { &["--archive-s3-secret-access-key", "secret"][..] },
100 s3_http = { &["--archive-s3-allow-http"][..] },
101 gcs_service_account = { &["--archive-gcs-service-account-path", "/etc/sa.json"][..] },
102 gcs_endpoint = { &["--archive-gcs-endpoint", "http://fake-gcs:4443"][..] },
103 gcs_http = { &["--archive-gcs-allow-http"][..] },
104 )]
105 fn backend_sub_flags_require_their_bucket(stray: &[&str]) {
106 let args = args_from(stray).expect("args");
109 check!(
110 matches!(args.validate(), Err(RestoreError::InvalidArgument(_))),
111 "{stray:?}"
112 );
113 }
114
115 #[test]
116 fn backend_sub_flags_are_accepted_with_their_bucket() {
117 let args = Cli::try_parse_from([
118 "krabka-restore",
119 "--log-dir",
120 "/target",
121 "--archive-s3-bucket",
122 "backups",
123 "--archive-s3-region",
124 "eu-west-1",
125 "--archive-s3-allow-http",
126 ])
127 .expect("args")
128 .args;
129 check!(args.validate().is_ok());
130 }
131
132 #[test]
133 fn validate_rejects_two_bounds_on_one_partition() {
134 let args =
135 args_from(&["--to-offset", "orders:0=10", "--to-offset", "orders:0=20"]).expect("args");
136 check!(args.validate().is_err());
137 }
138
139 #[test]
140 fn validate_accepts_bounds_on_distinct_partitions() {
141 let args =
142 args_from(&["--to-offset", "orders:0=10", "--to-offset", "orders:1=20"]).expect("args");
143 check!(args.validate().is_ok());
144 }
145
146 #[test]
147 fn validate_rejects_a_bound_on_an_unselected_topic() {
148 let args = args_from(&["--topic", "orders", "--to-offset", "payments:0=10"]).expect("args");
149 check!(args.validate().is_err());
150
151 let args =
152 args_from(&["--topic", "orders", "--exclude-offset", "payments:0=1..2"]).expect("args");
153 check!(args.validate().is_err());
154 }
155
156 #[test]
157 fn validate_accepts_a_bound_on_a_selected_topic() {
158 let args = args_from(&["--topic", "orders", "--to-offset", "orders:0=10"]).expect("args");
159 check!(args.validate().is_ok());
160 }
161}