Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify Ankaios cli #340

Merged
merged 18 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion ank/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub enum SetCommands {
#[arg(required = true)]
object_field_mask: Vec<String>,
/// A file containing the new State Object Description in yaml format
#[arg(short = 'f', long = "file")]
#[arg(required = true)]
state_object_file: Option<String>,
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
},
}
Expand Down
276 changes: 252 additions & 24 deletions ank/src/cli_commands/set_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use common::{
objects::{CompleteState, StoredWorkloadSpec},
state_manipulation::{Object, Path},
};
use std::io::{self, Read};

#[cfg(not(test))]
fn read_file_to_string(file: String) -> std::io::Result<String> {
Expand Down Expand Up @@ -54,6 +55,70 @@ fn add_default_workload_spec_per_update_mask(
}
}

// [impl->swdd~cli-supports-yaml-to-set-desired-state~1]
async fn process_inputs<R: Read>(reader: R, state_object_file: &str, temp_obj: &mut Object) {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
match state_object_file {
"-" => {
let stdin = io::read_to_string(reader).unwrap_or_else(|error| {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
output_and_error!("Could not read the state object file.\nError: {}", error)
});
let value: serde_yaml::Value = serde_yaml::from_str(&stdin).unwrap_or_else(|error| {
output_and_error!("Could not convert to yaml Value.\nError: {}", error)
});
*temp_obj = Object::try_from(&value).unwrap_or_else(|error| {
output_and_error!("Could not convert object.\n Error: {}", error)
});
}
_ => {
let state_object_data = read_file_to_string(state_object_file.to_string())
.unwrap_or_else(|error| {
output_and_error!("Could not read the state object file.\nError: {}", error)
});
let value: serde_yaml::Value =
serde_yaml::from_str(&state_object_data).unwrap_or_else(|error| {
output_and_error!("Could not convert to yaml Value.\nError: {}", error)
});
*temp_obj = Object::try_from(&value).unwrap_or_else(|error| {
output_and_error!("Could not convert object.\n Error: {}", error)
});
}
}
}

fn overwrite_using_field_mask(
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
complete_state_object: &mut Object,
object_field_mask: &Vec<String>,
temp_obj: &Object,
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
) {
for field_mask in object_field_mask {
let path: Path = field_mask.into();

complete_state_object
.set(
&path,
temp_obj
.get(&path)
.ok_or(CliError::ExecutionError(format!(
"Specified update mask '{field_mask}' not found in the input config.",
)))
.unwrap_or_else(|error| {
output_and_error!(
"Encountered error while overwritting using field mask. Error: {}",
error
)
})
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
.clone(),
)
.map_err(|err| CliError::ExecutionError(err.to_string()))
.unwrap_or_else(|error| {
output_and_error!(
"Encountered error while overwritting using field mask. Error: {}",
error
)
});
}
}

impl CliCommands {
pub async fn set_state(
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
&mut self,
Expand All @@ -67,33 +132,15 @@ impl CliCommands {
);

let mut complete_state = CompleteState::default();
if let Some(state_object_file) = state_object_file {
let state_object_data =
read_file_to_string(state_object_file).unwrap_or_else(|error| {
output_and_error!("Could not read the state object file.\nError: {}", error)
});
let value: serde_yaml::Value = serde_yaml::from_str(&state_object_data)?;
let x = Object::try_from(&value)?;
let mut temp_obj: Object = Object::default();

// This here is a workaround for the default workload specs
if let Some(state_object_file) = state_object_file {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
process_inputs(io::stdin(), &state_object_file, &mut temp_obj).await;
add_default_workload_spec_per_update_mask(&object_field_mask, &mut complete_state);

// now overwrite with the values from the field mask
let mut complete_state_object: Object = complete_state.try_into()?;
for field_mask in &object_field_mask {
let path: Path = field_mask.into();

complete_state_object
.set(
&path,
x.get(&path)
.ok_or(CliError::ExecutionError(format!(
"Specified update mask '{field_mask}' not found in the input config.",
)))?
.clone(),
)
.map_err(|err| CliError::ExecutionError(err.to_string()))?;
}
overwrite_using_field_mask(&mut complete_state_object, &object_field_mask, &temp_obj);
complete_state = complete_state_object.try_into()?;
}

Expand All @@ -117,9 +164,190 @@ impl CliCommands {
//////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use std::io;
use super::*;
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
use crate::cli_commands::server_connection::MockServerConnection;
use api::ank_base::UpdateStateSuccess;
use common::{
// commands::UpdateStateSuccess,
objects::{CompleteState, RestartPolicy, State},
state_manipulation::Object,
};
use mockall::predicate::eq;
use serde_yaml::Value;
use std::{collections::HashMap, io::Cursor};

pub fn read_to_string_mock(_file: String) -> io::Result<String> {
Ok("".into())
Ok(_file)
}

const RESPONSE_TIMEOUT_MS: u64 = 3000;

const SAMPLE_CONFIG: &str = r#"desiredState:
workloads:
nginx:
agent: agent_A
tags:
- key: owner
value: Ankaios team
dependencies: {}
restartPolicy: ALWAYS
runtime: podman
runtimeConfig: |
image: docker.io/nginx:latest
commandOptions: ["-p", "8081:80"]"#;

#[test]
fn utest_add_default_workload_spec_empty_update_mask() {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
let update_mask = vec![];
let mut complete_state = CompleteState::default();

add_default_workload_spec_per_update_mask(&update_mask, &mut complete_state);

assert!(complete_state.desired_state.workloads.is_empty());
}

#[test]
fn utest_add_default_workload_spec_with_update_mask() {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
let update_mask = vec![
"desiredState.workloads.nginx.restartPolicy".to_string(),
"desiredState.workloads.nginx2.restartPolicy".to_string(),
"desiredState.workloads.nginx3".to_string(),
];
let mut complete_state = CompleteState::default();

add_default_workload_spec_per_update_mask(&update_mask, &mut complete_state);

assert!(complete_state.desired_state.workloads.contains_key("nginx"));
assert!(complete_state
.desired_state
.workloads
.contains_key("nginx2"));
HorjuRares marked this conversation as resolved.
Show resolved Hide resolved
assert!(!complete_state
.desired_state
.workloads
.contains_key("nginx3"));
}

#[test]
fn utest_add_default_workload_spec_invalid_path() {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
let update_mask = vec!["invalid.path".to_string()];
let mut complete_state = CompleteState::default();

add_default_workload_spec_per_update_mask(&update_mask, &mut complete_state);

assert!(complete_state.desired_state.workloads.is_empty());
}

#[test]
fn utest_overwrite_using_field_mask() {
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
let workload_spec = StoredWorkloadSpec::default();
let mut complete_state = CompleteState {
desired_state: State {
workloads: HashMap::from([("nginx".to_string(), workload_spec)]),
..Default::default()
},
..Default::default()
};
let mut complete_state_object: Object = complete_state.try_into().unwrap();
let value: serde_yaml::Value = serde_yaml::from_str(SAMPLE_CONFIG).unwrap();
let temp_object = Object::try_from(&value).unwrap();
let update_mask = vec!["desiredState.workloads.nginx".to_string()];

overwrite_using_field_mask(&mut complete_state_object, &update_mask, &temp_object);

complete_state = complete_state_object.try_into().unwrap();

assert!(complete_state.desired_state.workloads.contains_key("nginx"));
assert_eq!(
complete_state
.desired_state
.workloads
.get("nginx")
.unwrap()
.restart_policy,
RestartPolicy::Always
)
}

// [utest->swdd~cli-supports-yaml-to-set-desired-state~1]
#[tokio::test]
async fn utest_process_inputs_stdin() {
let input = SAMPLE_CONFIG;
let reader = Cursor::new(input);
let state_object_file = "-".to_string();
let mut temp_obj = Object::default();

process_inputs(reader, &state_object_file, &mut temp_obj).await;

let value: Value = serde_yaml::from_str(SAMPLE_CONFIG).unwrap();
let expected_obj = Object::try_from(&value).unwrap();

assert_eq!(temp_obj, expected_obj);
}

// [utest->swdd~cli-supports-yaml-to-set-desired-state~1]
#[tokio::test]
async fn utest_process_inputs_file() {
let state_object_file = SAMPLE_CONFIG.to_owned();
let mut temp_obj = Object::default();
println!("{:?}", state_object_file);

process_inputs(io::empty(), &state_object_file, &mut temp_obj).await;
println!("{:?}", temp_obj);

let value: Value = serde_yaml::from_str(SAMPLE_CONFIG).unwrap();
let expected_obj = Object::try_from(&value).unwrap();
println!("{:?}", expected_obj);

assert_eq!(temp_obj, expected_obj);
}

// [utest->swdd~cli-supports-yaml-to-set-desired-state~1]
#[tokio::test]
async fn utest_process_inputs_invalid_yaml() {
let input = "invalid yaml";
let reader = Cursor::new(input);
let state_object_file = "-".to_string();
let mut temp_obj = Object::default();

process_inputs(reader, &state_object_file, &mut temp_obj).await;
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
}

// [utest->swdd~cli-provides-set-desired-state~1]
#[tokio::test]
async fn utest_set_state_ok() {
let update_mask = vec!["desiredState.workloads.nginx.restartPolicy".to_string()];
let state_object_file = Some(SAMPLE_CONFIG.to_owned());

let workload_spec = StoredWorkloadSpec {
restart_policy: RestartPolicy::Always,
..Default::default()
};
let updated_state = CompleteState {
desired_state: State {
workloads: HashMap::from([("nginx".to_string(), workload_spec)]),
..Default::default()
},
..Default::default()
};
let mut mock_server_connection = MockServerConnection::default();
mock_server_connection
.expect_update_state()
.with(eq(updated_state), eq(update_mask.clone()))
.return_once(|_, _| {
Ok(UpdateStateSuccess {
added_workloads: vec![],
deleted_workloads: vec![],
})
inf17101 marked this conversation as resolved.
Show resolved Hide resolved
});

let mut cmd = CliCommands {
_response_timeout_ms: RESPONSE_TIMEOUT_MS,
no_wait: true,
server_connection: mock_server_connection,
};

let set_state_result = cmd.set_state(update_mask, state_object_file).await;
assert!(set_state_result.is_ok());
}
}
2 changes: 1 addition & 1 deletion doc/docs/reference/complete-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The object field mask can be constructed using the field names of the [CompleteS
commandOptions: ["-p", "8081:80"]
```

3. Example `ank -k set state -f new-state.yaml desiredState.workloads.nginx.restartPolicy` changes the restart behavior of nginx workload to `NEVER`:
3. Example `ank set state desiredState.workloads.nginx.restartPolicy new-state.yaml` changes the restart behavior of nginx workload to `NEVER`:

```yaml title="new-state.yaml"
desiredState:
Expand Down
6 changes: 3 additions & 3 deletions tests/stests/workloads/interworkload_dependencies.robot
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Test Ankaios CLI update workload with pending delete
# Actions
When user triggers "ank -k get state > ${new_state_yaml_file}"
And user updates the state "${new_state_yaml_file}" with "desiredState.workloads.backend.runtimeConfig.commandOptions=['-p', '8084:80']"
And user triggers "ank -k --no-wait set state -f ${new_state_yaml_file} desiredState.workloads.backend"
And user triggers "ank -k --no-wait set state ${new_state_yaml_file} desiredState.workloads.backend"
And the workload "backend" shall have the execution state "Stopping(WaitingToStop)" on agent "agent_A" within "20" seconds
And user triggers "ank -k delete workload frontend"
# Asserts
Expand All @@ -110,9 +110,9 @@ Test Ankaios CLI update workload with pending create
And Ankaios agent is started with name "agent_A"
And the workload "after_backend" shall have the execution state "Succeeded(Ok)" on agent "agent_A" within "20" seconds
# Actions
When user triggers "ank -k --no-wait set state -f ${new_state_yaml_file} desiredState.workloads.after_backend"
When user triggers "ank -k --no-wait set state ${new_state_yaml_file} desiredState.workloads.after_backend"
And the workload "after_backend" shall have the execution state "Pending(WaitingToStart)" on agent "agent_A" within "3" seconds
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.backend"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.backend"
# Asserts
Then the workload "backend" shall have the execution state "Succeeded(Ok)" on agent "agent_A" within "5" seconds
And the workload "after_backend" shall have the execution state "Succeeded(Ok)" on agent "agent_A" within "5" seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Test Ankaios CLI update state with cycle in interworkload dependencies is reject
And Ankaios agent is started with name "agent_A"
And all workloads of agent "agent_A" have an initial execution state
# Actions
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.workload_C"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.workload_C"
# Asserts
Then the workload "workload_C" shall not exist
And podman shall not have a container for workload "workload_C" on agent "agentA" within "5" seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Test Ankaios Podman retry creation of a workload on creation failure
And user triggers "ank -k delete workload hello1"
And the workload "hello1" shall not exist on agent "agent_A" within "20" seconds
And podman shall not have a container for workload "hello1" on agent "agent_A" within "20" seconds
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.hello1"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.hello1"
# Asserts
Then the workload "hello1" shall have the execution state "Running(Ok)" from agent "agent_A" within "20" seconds
[Teardown] Clean up Ankaios
Expand All @@ -62,9 +62,9 @@ Test Ankaios Podman retry creation of a workload on creation failure intercepted
And user triggers "ank -k delete workload hello1"
And the workload "hello1" shall not exist on agent "agent_A" within "20" seconds
And podman shall not have a container for workload "hello1" on agent "agent_A" within "20" seconds
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.hello1"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.hello1"
And user updates the state "${new_state_yaml_file}" with "desiredState.workloads.hello1.runtimeConfig.commandArgs=['3']"
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.hello1"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.hello1"
# Asserts
Then the workload "hello1" shall have the execution state "Succeeded(Ok)" from agent "agent_A" within "20" seconds
[Teardown] Clean up Ankaios
Expand All @@ -87,7 +87,7 @@ Test Ankaios Podman retry creation of a workload on creation failure intercepted
And user triggers "ank -k delete workload hello1"
And the workload "hello1" shall not exist on agent "agent_A" within "20" seconds
And podman shall not have a container for workload "hello1" on agent "agent_A" within "20" seconds
And user triggers "ank -k set state -f ${new_state_yaml_file} desiredState.workloads.hello1"
And user triggers "ank -k set state ${new_state_yaml_file} desiredState.workloads.hello1"
And the user waits "1" seconds
And user triggers "ank -k delete workload hello1"
# Asserts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Test Ankaios CLI update workload
And Ankaios agent is started with name "agent_A"
And all workloads of agent "agent_A" have an initial execution state
# Actions
And user triggers "ank -k --no-wait set state -f ${new_state_yaml_file} desiredState.workloads.simple.agent"
And user triggers "ank -k --no-wait set state ${new_state_yaml_file} desiredState.workloads.simple.agent"
# Asserts
Then the workload "simple" shall not exist on agent "agent_A" within "20" seconds
And podman shall not have a container for workload "simple" on agent "agent_A"
Expand Down
Loading
Loading