Skip to content

Commit 61835ea

Browse files
committed
[#198]: refactor & enhance install logic and add new dependencies for improved functionality
1 parent 264a8a0 commit 61835ea

3 files changed

Lines changed: 184 additions & 80 deletions

File tree

‎cli/Cargo.lock‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/Cargo.toml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ cortexflow_agent_api = {version="0.1.2",features = ["client"]}
2626
kube = "2.0.1"
2727
k8s-openapi = {version = "0.26.0", features = ["v1_34"]}
2828

29+
[dev-dependencies]
30+
serde_json = "1.0"
31+
tower = { version = "0.5", features = ["util"] }
32+
http = "1"
33+
http-body-util = "0.1"
34+
bytes = "1"
35+
2936
[[bin]]
3037
name = "cfcli"
3138
path = "src/main.rs"

‎cli/src/install.rs‎

Lines changed: 172 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -214,54 +214,59 @@ async fn install_simple_example_component() -> Result<(), CliError> {
214214
// docs:
215215
pub async fn install_blocklist_configmap() -> Result<(), CliError> {
216216
match connect_to_client().await {
217-
Ok(client) => {
218-
println!(
219-
"{} {}",
220-
"=====>".blue().bold(),
221-
"Checking if the Blocklist configmap exists"
222-
);
223-
sleep(Duration::from_secs(1));
224-
let blocklist_exists = check_if_blocklist_exists(client).await?;
225-
if !blocklist_exists {
217+
Ok(client) => install_blocklist_configmap_with_client(client).await,
218+
Err(e) => Err(CliError::ClientError(Error::Api(ErrorResponse {
219+
status: "failed".to_string(),
220+
message: "Failed to connect to kubernetes client".to_string(),
221+
reason: e.to_string(),
222+
code: 404,
223+
}))),
224+
}
225+
}
226+
227+
// docs:
228+
//
229+
// Split out of install_blocklist_configmap so the blocklist-checking logic can be
230+
// exercised in tests against a fake kube Client, without requiring a real cluster.
231+
232+
async fn install_blocklist_configmap_with_client(client: Client) -> Result<(), CliError> {
233+
println!(
234+
"{} {}",
235+
"=====>".blue().bold(),
236+
"Checking if the Blocklist configmap exists"
237+
);
238+
sleep(Duration::from_secs(1));
239+
let blocklist_exists = check_if_blocklist_exists(client).await?;
240+
if !blocklist_exists {
241+
println!(
242+
"{} {}",
243+
"=====>".blue().bold(),
244+
"Blocklist configmap does not exist".red().bold()
245+
);
246+
sleep(Duration::from_secs(1));
247+
println!("{} {}", "=====>".bold().blue(), "Creating configmap");
248+
let metdata_configs = create_configs();
249+
sleep(Duration::from_secs(1));
250+
match create_config_file(metdata_configs).await {
251+
Ok(_) => {
226252
println!(
227253
"{} {}",
228-
"=====>".blue().bold(),
229-
"Blocklist configmap does not exist".red().bold()
230-
);
231-
sleep(Duration::from_secs(1));
232-
println!("{} {}", "=====>".bold().blue(), "Creating configmap");
233-
let metdata_configs = create_configs();
234-
sleep(Duration::from_secs(1));
235-
match create_config_file(metdata_configs).await {
236-
Ok(_) => {
237-
println!(
238-
"{} {}",
239-
"=====>".bold().blue(),
240-
"Configmap created/repaired successfully".bold().green()
241-
)
242-
}
243-
Err(e) => {
244-
return Err(CliError::InstallerError {
245-
reason: e.to_string(),
246-
});
247-
}
248-
}
249-
return Ok(());
250-
} else {
251-
println!()
254+
"=====>".bold().blue(),
255+
"Configmap created/repaired successfully".bold().green()
256+
)
257+
}
258+
Err(e) => {
259+
return Err(CliError::InstallerError {
260+
reason: e.to_string(),
261+
});
252262
}
253-
254-
Ok(())
255-
}
256-
Err(e) => {
257-
return Err(CliError::ClientError(Error::Api(ErrorResponse {
258-
status: "failed".to_string(),
259-
message: "Failed to connect to kubernetes client".to_string(),
260-
reason: e.to_string(),
261-
code: 404,
262-
})));
263263
}
264+
return Ok(());
265+
} else {
266+
println!()
264267
}
268+
269+
Ok(())
265270
}
266271

267272
// docs:
@@ -298,6 +303,13 @@ async fn check_if_blocklist_exists(client: Client) -> Result<bool, CliError> {
298303
//
299304

300305
fn install_components(components_type: &str) -> Result<(), CliError> {
306+
install_components_with(&RealCommandRunner, components_type)
307+
}
308+
309+
fn install_components_with(
310+
runner: &dyn CommandRunner,
311+
components_type: &str,
312+
) -> Result<(), CliError> {
301313
if components_type == "cortexbrain" {
302314
let files_to_install = vec![
303315
"configmap-role.yaml",
@@ -328,7 +340,7 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
328340
"Applying",
329341
component.to_string().green().bold()
330342
);
331-
apply_component(component)?;
343+
apply_component_with(runner, component)?;
332344
i = i + 1;
333345
}
334346
} else if components_type == "simple-example" {
@@ -348,7 +360,7 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
348360
"Applying",
349361
component.to_string().green().bold()
350362
);
351-
apply_component(component)?;
363+
apply_component_with(runner, component)?;
352364
i = i + 1;
353365
}
354366
} else {
@@ -368,10 +380,10 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
368380
//
369381
// Returns an CliError if something fails
370382

371-
fn apply_component(file: &str) -> Result<(), CliError> {
372-
let output = Command::new(BASE_COMMAND)
373-
.args(["apply", "-f", file])
374-
.output()
383+
fn apply_component_with(runner: &dyn CommandRunner, file: &str) -> Result<(), CliError> {
384+
let args = ["apply".to_string(), "-f".to_string(), file.to_string()];
385+
let output = runner
386+
.run(BASE_COMMAND, &args)
375387
.map_err(|e| CliError::InstallerError {
376388
reason: e.to_string(),
377389
})?;
@@ -523,14 +535,7 @@ fn rm_file(file_to_remove: &str) -> Result<(), CliError> {
523535
#[cfg(test)]
524536
mod tests {
525537
use super::*;
526-
527-
#[test]
528-
fn test_rm_file() {
529-
let file_name = "test_rm_file.txt";
530-
std::fs::write(file_name, "test content").unwrap();
531-
assert!(rm_file(file_name).is_ok());
532-
assert!(!std::path::Path::new(file_name).exists());
533-
}
538+
use clap::Parser;
534539

535540
#[test]
536541
fn test_download_file_with_success() {
@@ -550,20 +555,6 @@ mod tests {
550555
assert!(download_file_with(&runner, "https://example.com/file.yaml").is_err());
551556
}
552557

553-
#[test]
554-
fn test_rm_file_failure() {
555-
let file_name = "non_existent_file.txt";
556-
assert!(rm_file(file_name).is_err());
557-
}
558-
559-
#[test]
560-
fn test_rm_file_cleanup_only() {
561-
let file_name = "test_rm_file_cleanup_only.txt";
562-
std::fs::write(file_name, "test content").unwrap();
563-
// Clean up without removing
564-
rm_file(file_name).unwrap();
565-
}
566-
567558
#[test]
568559
fn test_rm_file_failure_cleanup() {
569560
let file_name = "test_rm_file_failure_cleanup.txt";
@@ -579,18 +570,119 @@ mod tests {
579570
assert!(rm_file(file_name).is_ok());
580571
}
581572

573+
// docs: builds a fake kube Client backed by an in-memory tower service, so the
574+
// Kubernetes API calls never leave the process or touch a real cluster.
575+
fn mock_client(status: http::StatusCode, body: String) -> Client {
576+
let service = tower::service_fn(move |_req: http::Request<kube::client::Body>| {
577+
let body = body.clone();
578+
async move {
579+
let response = http::Response::builder()
580+
.status(status)
581+
.body(http_body_util::Full::new(bytes::Bytes::from(body)))
582+
.unwrap();
583+
Ok::<_, std::convert::Infallible>(response)
584+
}
585+
});
586+
Client::new(service, "cortexflow")
587+
}
588+
589+
fn configmap_found_body() -> String {
590+
serde_json::json!({
591+
"apiVersion": "v1",
592+
"kind": "ConfigMap",
593+
"metadata": {
594+
"name": "cortexbrain-client-config",
595+
"namespace": "cortexflow"
596+
},
597+
"data": {}
598+
})
599+
.to_string()
600+
}
601+
602+
fn configmap_not_found_body() -> String {
603+
serde_json::json!({
604+
"apiVersion": "v1",
605+
"kind": "Status",
606+
"status": "Failure",
607+
"message": "configmaps \"cortexbrain-client-config\" not found",
608+
"reason": "NotFound",
609+
"code": 404
610+
})
611+
.to_string()
612+
}
613+
614+
#[tokio::test]
615+
async fn test_check_if_blocklist_exists_true() {
616+
let client = mock_client(http::StatusCode::OK, configmap_found_body());
617+
let exists = check_if_blocklist_exists(client).await.unwrap();
618+
assert!(exists);
619+
}
620+
621+
#[tokio::test]
622+
async fn test_check_if_blocklist_exists_false() {
623+
let client = mock_client(http::StatusCode::NOT_FOUND, configmap_not_found_body());
624+
let exists = check_if_blocklist_exists(client).await.unwrap();
625+
assert!(!exists);
626+
}
627+
628+
#[tokio::test]
629+
async fn test_install_blocklist_configmap_with_client_already_exists() {
630+
// When the configmap already exists no creation attempt is made, so this
631+
// is safe to exercise end-to-end against the fake client.
632+
let client = mock_client(http::StatusCode::OK, configmap_found_body());
633+
assert!(install_blocklist_configmap_with_client(client).await.is_ok());
634+
}
635+
582636
#[test]
583-
fn test_rm_file_success_cleanup_only() {
584-
let file_name = "test_rm_file_success_cleanup_only.txt";
585-
std::fs::write(file_name, "test content").unwrap();
586-
// Clean up a file that exists
587-
assert!(rm_file(file_name).is_ok());
637+
fn test_install_components_with_invalid_type() {
638+
let runner = crate::command_runner::test_support::StubCommandRunner::success("");
639+
assert!(install_components_with(&runner, "not-a-real-type").is_err());
588640
}
589641

590642
#[test]
591-
fn test_rm_file_failure_cleanup_only() {
592-
let file_name = "non_existent_file.txt";
593-
// Attempt to clean up a non-existent file
594-
assert!(rm_file(file_name).is_err());
643+
fn test_install_components_with_simple_example_success() {
644+
let runner = crate::command_runner::test_support::StubCommandRunner::success("");
645+
assert!(install_components_with(&runner, "simple-example").is_ok());
646+
}
647+
648+
#[test]
649+
fn test_install_components_with_cortexbrain_success() {
650+
let runner = crate::command_runner::test_support::StubCommandRunner::success("");
651+
assert!(install_components_with(&runner, "cortexbrain").is_ok());
652+
}
653+
654+
#[test]
655+
fn test_install_components_with_apply_failure() {
656+
let runner = crate::command_runner::test_support::StubCommandRunner::failure("kubectl error");
657+
assert!(install_components_with(&runner, "simple-example").is_err());
658+
}
659+
660+
#[derive(clap::Parser)]
661+
struct InstallCommandsHarness {
662+
#[command(subcommand)]
663+
cmd: InstallCommands,
664+
}
665+
666+
#[test]
667+
fn test_install_commands_parses_cortexflow() {
668+
let parsed = InstallCommandsHarness::try_parse_from(["cfcli", "cortexflow"]).unwrap();
669+
assert!(matches!(parsed.cmd, InstallCommands::All));
670+
}
671+
672+
#[test]
673+
fn test_install_commands_parses_simple_example() {
674+
let parsed = InstallCommandsHarness::try_parse_from(["cfcli", "simple-example"]).unwrap();
675+
assert!(matches!(parsed.cmd, InstallCommands::TestPods));
676+
}
677+
678+
#[test]
679+
fn test_install_commands_parses_blocklist() {
680+
let parsed = InstallCommandsHarness::try_parse_from(["cfcli", "blocklist"]).unwrap();
681+
assert!(matches!(parsed.cmd, InstallCommands::Blocklist));
682+
}
683+
684+
#[test]
685+
fn test_install_commands_rejects_unknown_subcommand() {
686+
assert!(InstallCommandsHarness::try_parse_from(["cfcli", "not-a-command"]).is_err());
595687
}
596688
}

0 commit comments

Comments
 (0)