Skip to content

Commit a6b8510

Browse files
committed
Support --dry-run in add command
1 parent 8e6696c commit a6b8510

15 files changed

Lines changed: 760 additions & 158 deletions

File tree

src/app/cli/src/cli.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub enum PasswordHashingMode {
9191
#[derive(Debug, clap::Subcommand)]
9292
pub enum Command {
9393
Add(Add),
94+
Apply(Apply),
9495
Complete(Complete),
9596
Completions(Completions),
9697
Config(Config),
@@ -169,6 +170,10 @@ Add a dataset from manifest hosted externally (e.g. on GihHub):
169170
To add dataset from a repository see `kamu pull` command.
170171
"#)]
171172
pub struct Add {
173+
/// Show the changes to be applied without actually doing them
174+
#[arg(long)]
175+
pub dry_run: bool,
176+
172177
/// Recursively search for all manifest in the specified directory
173178
#[arg(long, short = 'r')]
174179
pub recursive: bool,
@@ -185,7 +190,44 @@ pub struct Add {
185190
#[arg(long, value_name = "N")]
186191
pub name: Option<odf::DatasetAlias>,
187192

188-
/// Changing the visibility of the added dataset
193+
/// Visibility of the added dataset
194+
#[arg(long, value_name = "VIS", value_enum)]
195+
pub visibility: Option<parsers::DatasetVisibility>,
196+
197+
/// Dataset manifest reference(s) (path, or URL)
198+
#[arg()]
199+
pub manifest: Vec<String>,
200+
}
201+
202+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
203+
204+
/// Add a new dataset or modify an existing one
205+
#[derive(Debug, clap::Args)]
206+
#[command(after_help = r#"
207+
**Examples:**
208+
209+
Compare the state of a dataset to a manifest:
210+
211+
kamu apply --dry-run org.example.data.yaml
212+
213+
Synchronize all objects with the state described in manifests found in the current directory:
214+
215+
kamu apply --recursive .
216+
"#)]
217+
pub struct Apply {
218+
/// Show the changes to be applied without actually doing them
219+
#[arg(long)]
220+
pub dry_run: bool,
221+
222+
/// Recursively search for all manifest in the specified directory
223+
#[arg(long, short = 'r')]
224+
pub recursive: bool,
225+
226+
/// Read manifests from standard input
227+
#[arg(long)]
228+
pub stdin: bool,
229+
230+
/// Visibility of the added dataset
189231
#[arg(long, value_name = "VIS", value_enum)]
190232
pub visibility: Option<parsers::DatasetVisibility>,
191233

src/app/cli/src/cli_commands.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub fn get_command(
3434
AddCommand::builder(
3535
c.manifest,
3636
c.name,
37+
c.dry_run,
3738
c.recursive,
3839
c.replace,
3940
c.stdin,
@@ -44,6 +45,19 @@ pub fn get_command(
4445
.cast(),
4546
),
4647

48+
cli::Command::Apply(c) => Box::new(
49+
ApplyCommand::builder(
50+
c.manifest,
51+
c.dry_run,
52+
c.recursive,
53+
c.stdin,
54+
c.visibility
55+
.map(Into::into)
56+
.unwrap_or(tenancy_config.default_dataset_visibility()),
57+
)
58+
.cast(),
59+
),
60+
4761
cli::Command::Complete(c) => Box::new(CompleteCommand::builder(c.input, c.current).cast()),
4862

4963
cli::Command::Completions(c) => Box::new(CompletionsCommand::builder(c.shell).cast()),

src/app/cli/src/commands/add_command.rs

Lines changed: 67 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
// the Business Source License, use of this software will be governed
88
// by the Apache License, Version 2.0.
99

10-
use std::collections::{HashSet, LinkedList};
1110
use std::sync::Arc;
1211

12+
use internal_error::*;
1313
use kamu::domain::*;
1414
use kamu_datasets::{
1515
CreateDatasetFromSnapshotError,
1616
CreateDatasetFromSnapshotUseCase,
17-
CreateDatasetResult,
1817
CreateDatasetUseCaseOptions,
1918
DeleteDatasetUseCase,
2019
};
@@ -40,6 +39,9 @@ pub struct AddCommand {
4039
#[dill::component(explicit)]
4140
name: Option<odf::DatasetAlias>,
4241

42+
#[dill::component(explicit)]
43+
dry_run: bool,
44+
4345
#[dill::component(explicit)]
4446
recursive: bool,
4547

@@ -129,70 +131,6 @@ impl AddCommand {
129131

130132
Ok(false)
131133
}
132-
133-
pub async fn create_datasets_from_snapshots(
134-
&self,
135-
snapshots: Vec<odf::DatasetSnapshot>,
136-
create_options: CreateDatasetUseCaseOptions,
137-
) -> Vec<(
138-
odf::DatasetAlias,
139-
Result<CreateDatasetResult, CreateDatasetFromSnapshotError>,
140-
)> {
141-
let snapshots_ordered =
142-
self.sort_snapshots_in_dependency_order(snapshots.into_iter().collect());
143-
144-
let mut ret = Vec::new();
145-
for snapshot in snapshots_ordered {
146-
let alias = snapshot.name.clone();
147-
let res = self
148-
.create_dataset_from_snapshot
149-
.execute(snapshot, create_options)
150-
.await;
151-
152-
ret.push((alias, res));
153-
}
154-
ret
155-
}
156-
157-
#[allow(clippy::linkedlist)]
158-
fn sort_snapshots_in_dependency_order(
159-
&self,
160-
mut snapshots: LinkedList<odf::DatasetSnapshot>,
161-
) -> Vec<odf::DatasetSnapshot> {
162-
let mut ordered = Vec::with_capacity(snapshots.len());
163-
let mut pending: HashSet<odf::DatasetRef> =
164-
snapshots.iter().map(|s| s.name.clone().into()).collect();
165-
let mut added: HashSet<odf::DatasetAlias> = HashSet::new();
166-
167-
// TODO: cycle detection
168-
while !snapshots.is_empty() {
169-
let snapshot = snapshots.pop_front().unwrap();
170-
171-
use odf::metadata::EnumWithVariants;
172-
let transform = snapshot
173-
.metadata
174-
.iter()
175-
.find_map(|e| e.as_variant::<odf::metadata::SetTransform>());
176-
177-
let has_pending_deps = if let Some(transform) = transform {
178-
transform.inputs.iter().any(|input| {
179-
pending.contains(&input.dataset_ref)
180-
&& snapshot.name.as_local_ref() != input.dataset_ref // Check for circular dependency
181-
})
182-
} else {
183-
false
184-
};
185-
186-
if !has_pending_deps {
187-
pending.remove(&snapshot.name.clone().into());
188-
added.insert(snapshot.name.clone());
189-
ordered.push(snapshot);
190-
} else {
191-
snapshots.push_back(snapshot);
192-
}
193-
}
194-
ordered
195-
}
196134
}
197135

198136
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -252,6 +190,7 @@ impl Command for AddCommand {
252190
}
253191

254192
// Delete existing datasets if we are replacing
193+
// TODO: Move into use case?
255194
if self.replace {
256195
let mut already_exist = Vec::new();
257196
for s in &snapshots {
@@ -277,61 +216,87 @@ impl Command for AddCommand {
277216
}
278217
};
279218

280-
let create_options = CreateDatasetUseCaseOptions {
281-
dataset_visibility: self.dataset_visibility,
282-
};
283-
let mut add_results = self
284-
.create_datasets_from_snapshots(snapshots, create_options)
285-
.await;
286-
287-
add_results.sort_by(|(id_a, _), (id_b, _)| id_a.cmp(id_b));
288-
289-
let mut num_added = 0;
219+
let mut plan = self
220+
.create_dataset_from_snapshot
221+
.prepare(
222+
snapshots,
223+
CreateDatasetUseCaseOptions {
224+
dataset_visibility: self.dataset_visibility,
225+
},
226+
)
227+
.await?;
290228

291229
let mut errors_with_contexts = Vec::new();
230+
let mut errors = Vec::new();
231+
std::mem::swap(&mut plan.errors, &mut errors);
292232

293-
for (id, res) in add_results {
294-
match res {
295-
Ok(_) => {
296-
num_added += 1;
297-
298-
if !self.output_config.quiet {
299-
eprintln!("{}: {}", console::style("Added").green(), id);
300-
}
301-
}
302-
Err(CreateDatasetFromSnapshotError::NameCollision(_)) => {
233+
for (snaphot, err) in errors {
234+
match err {
235+
CreateDatasetFromSnapshotError::NameCollision(_) => {
303236
if !self.output_config.quiet {
304237
eprintln!(
305238
"{}: {}: Already exists",
306239
console::style("Skipped").yellow(),
307-
id
240+
snaphot.name,
308241
);
309242
}
310243
}
311-
Err(err) => {
312-
errors_with_contexts.push((err, format!("Failed to add dataset {id}")));
244+
err => {
245+
errors_with_contexts
246+
.push((err, format!("Failed to add dataset {}", snaphot.name)));
313247
}
314248
}
315249
}
316250

317-
if errors_with_contexts.is_empty() {
318-
if !self.output_config.quiet {
251+
if !errors_with_contexts.is_empty() {
252+
return Err(BatchError::new(
253+
format!("Failed to add {} manifest(s)", errors_with_contexts.len()),
254+
errors_with_contexts,
255+
)
256+
.into());
257+
}
258+
259+
if self.dry_run {
260+
let plan = serde_yaml::to_string(&plan).int_err()?;
261+
eprintln!(
262+
"{}\n{}\n{}",
263+
console::style("Execution plan:").yellow().bold(),
264+
console::style(&plan).dim(),
265+
console::style("Exiting early due to --dry-run")
266+
.yellow()
267+
.bold()
268+
);
269+
return Ok(());
270+
}
271+
272+
let mut add_results = self
273+
.create_dataset_from_snapshot
274+
.apply(plan)
275+
.await
276+
.int_err()?;
277+
278+
add_results.sort_by(|a, b| a.dataset_handle.alias.cmp(&b.dataset_handle.alias));
279+
280+
if !self.output_config.quiet {
281+
for res in &add_results {
319282
eprintln!(
320-
"{}",
321-
console::style(format!("Added {num_added} dataset(s)"))
322-
.green()
323-
.bold()
283+
"{}: {}",
284+
console::style("Added").green(),
285+
res.dataset_handle.alias
324286
);
325287
}
288+
}
326289

327-
Ok(())
328-
} else {
329-
Err(BatchError::new(
330-
format!("Failed to load {} manifest(s)", errors_with_contexts.len()),
331-
errors_with_contexts,
332-
)
333-
.into())
290+
if !self.output_config.quiet {
291+
eprintln!(
292+
"{}",
293+
console::style(format!("Added {} dataset(s)", add_results.len()))
294+
.green()
295+
.bold()
296+
);
334297
}
298+
299+
Ok(())
335300
}
336301
}
337302

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright Kamu Data, Inc. and contributors. All rights reserved.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the LICENSE file.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0.
9+
10+
use std::sync::Arc;
11+
12+
use kamu::domain::*;
13+
14+
use super::{CLIError, Command};
15+
use crate::OutputConfig;
16+
17+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
18+
19+
#[expect(dead_code)]
20+
#[dill::component]
21+
#[dill::interface(dyn Command)]
22+
pub struct ApplyCommand {
23+
resource_loader: Arc<dyn ResourceLoader>,
24+
dataset_registry: Arc<dyn DatasetRegistry>,
25+
create_dataset_from_snapshot: Arc<dyn kamu_datasets::CreateDatasetFromSnapshotUseCase>,
26+
delete_dataset: Arc<dyn kamu_datasets::DeleteDatasetUseCase>,
27+
output_config: Arc<OutputConfig>,
28+
tenancy_config: TenancyConfig,
29+
30+
#[dill::component(explicit)]
31+
snapshot_refs: Vec<String>,
32+
33+
#[dill::component(explicit)]
34+
dry_run: bool,
35+
36+
#[dill::component(explicit)]
37+
recursive: bool,
38+
39+
#[dill::component(explicit)]
40+
stdin: bool,
41+
42+
#[dill::component(explicit)]
43+
dataset_visibility: odf::DatasetVisibility,
44+
}
45+
46+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
47+
48+
#[async_trait::async_trait(?Send)]
49+
impl Command for ApplyCommand {
50+
async fn validate_args(&self) -> Result<(), CLIError> {
51+
unimplemented!()
52+
}
53+
54+
async fn run(&self) -> Result<(), CLIError> {
55+
unimplemented!()
56+
}
57+
}
58+
59+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

src/app/cli/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod add_command;
1111
mod alias_add_command;
1212
mod alias_delete_command;
1313
mod alias_list_command;
14+
mod apply_command;
1415
mod common;
1516
mod compact_command;
1617
mod complete_command;
@@ -62,6 +63,7 @@ pub use add_command::*;
6263
pub use alias_add_command::*;
6364
pub use alias_delete_command::*;
6465
pub use alias_list_command::*;
66+
pub use apply_command::*;
6567
pub use compact_command::*;
6668
pub use complete_command::*;
6769
pub use completions_command::*;

0 commit comments

Comments
 (0)