Correctly match capabilities when the agent has more than the project

This commit is contained in:
R Tyler Croy 2023-03-12 20:56:26 -07:00
parent 0d00fb4cfd
commit 23c2d64eef
5 changed files with 135 additions and 99 deletions

View File

@ -39,7 +39,7 @@ pub struct Project {
/*
* Used for optionally defining an inline Yml configuration
*/
inline: Option<Yml>,
pub inline: Option<Yml>,
pub filename: Option<String>,
#[serde(default = "default_scm", with = "serde_yaml::with::singleton_map")]
pub scm: Scm,
@ -58,13 +58,23 @@ fn default_scm() -> Scm {
* Loaded meaning the server has pinged the agent and gotten necessary bootstrap
* information
*/
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Agent {
pub name: String,
pub url: Url,
pub capabilities: Vec<synchronik::Capability>,
}
impl Default for Agent {
fn default() -> Self {
Self {
name: "default-agent".into(),
url: Url::parse("http://example.com").unwrap(),
capabilities: vec![],
}
}
}
impl Agent {
pub fn new(name: String, url: Url, capabilities: Vec<synchronik::Capability>) -> Self {
Self {
@ -82,18 +92,22 @@ impl Agent {
// data: data).await.unwrap_or("".into())
}
/*
* Determine if this agent can meet the specified needs
*/
pub fn can_meet(&self, needs: &Vec<String>) -> bool {
// TODO: Improve the performance of this by reducing the clones
let mut needs = needs.clone();
needs.sort();
let mut capabilities: Vec<String> = self
let capabilities: Vec<String> = self
.capabilities
.iter()
.map(|c| c.name.to_lowercase())
.collect();
capabilities.sort();
capabilities == needs
for need in needs {
if !capabilities.contains(&need) {
return false;
}
}
true
}
}
@ -202,6 +216,7 @@ fn merge_yaml(a: &mut serde_yaml::Value, b: serde_yaml::Value) {
mod tests {
use super::*;
use std::path::PathBuf;
use synchronik::Capability;
#[test]
fn test_serverconfig_from_filepath() {
@ -307,4 +322,71 @@ projects:
}
}
}
#[test]
fn agent_can_meet_false() {
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
let capabilities = vec![Capability::with_name("rustc")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert_eq!(false, agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_true() {
let needs: Vec<String> = vec!["dotnet".into()];
let capabilities = vec![Capability::with_name("dotnet")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert!(agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_false_multiple() {
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
let capabilities = vec![Capability::with_name("dotnet")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert_eq!(false, agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_true_multiple() {
let needs: Vec<String> = vec!["rspec".into(), "dotnet".into()];
let capabilities = vec![
Capability::with_name("dotnet"),
Capability::with_name("rspec"),
];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert!(agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_intersection() {
let needs: Vec<String> = vec!["git".into()];
let capabilities = vec![
Capability::with_name("dotnet"),
Capability::with_name("git"),
Capability::with_name("rspec"),
];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert!(agent.can_meet(&needs));
}
}

View File

@ -171,60 +171,3 @@ async fn main() -> Result<(), tide::Error> {
app.listen(opts.listen).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use synchronik::*;
#[test]
fn agent_can_meet_false() {
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
let capabilities = vec![Capability::with_name("rustc")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert_eq!(false, agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_true() {
let needs: Vec<String> = vec!["dotnet".into()];
let capabilities = vec![Capability::with_name("dotnet")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert!(agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_false_multiple() {
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
let capabilities = vec![Capability::with_name("dotnet")];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert_eq!(false, agent.can_meet(&needs));
}
#[test]
fn agent_can_meet_true_multiple() {
let needs: Vec<String> = vec!["rspec".into(), "dotnet".into()];
let capabilities = vec![
Capability::with_name("dotnet"),
Capability::with_name("rspec"),
];
let agent = Agent {
name: "test".into(),
url: Url::parse("http://localhost").unwrap(),
capabilities,
};
assert!(agent.can_meet(&needs));
}
}

View File

@ -49,6 +49,7 @@ pub async fn project(req: Request<AppState<'_>>) -> Result<Body, tide::Error> {
pub mod api {
use crate::config::{Scm, Yml};
use crate::Agent;
use crate::AppState;
use log::*;
use serde::Deserialize;
@ -74,7 +75,16 @@ pub mod api {
if let Some(project) = state.config.projects.get(&name) {
match &project.scm {
Scm::Nonexistent => {}
Scm::Nonexistent => {
info!("Nonexistent SCM, using inline configuration for {}", name);
info!("configuration: {:?}", project.inline);
if let Some(config) = &project.inline {
execute_commands(config, &state.agents).await?;
if let Some(red) = &next.next {
return Ok(tide::Redirect::new(red).into());
}
}
}
Scm::GitHub {
owner,
repo,
@ -91,36 +101,9 @@ pub mod api {
.await?;
let config_file: Yml = serde_yaml::from_str(&res.text().await?)?;
debug!("text: {:?}", config_file);
for agent in &state.agents {
if agent.can_meet(&config_file.needs) {
debug!("agent: {:?} can meet our needs", agent);
let commands: Vec<synchronik::Command> = config_file
.commands
.iter()
.map(|c| synchronik::Command::with_script(c))
.collect();
let commands = synchronik::CommandRequest { commands };
let client = reqwest::Client::new();
let _res = client
.put(
agent
.url
.join("/api/v1/execute")
.expect("Failed to join execute URL"),
)
.json(&commands)
.send()
.await?;
if let Some(red) = &next.next {
return Ok(tide::Redirect::new(red).into());
}
return Ok(
json!({ "msg": format!("Executing on {}", &agent.url) }).into()
);
}
execute_commands(&config_file, &state.agents).await?;
if let Some(red) = &next.next {
return Ok(tide::Redirect::new(red).into());
}
}
}
@ -128,4 +111,32 @@ pub mod api {
}
Ok(Response::new(StatusCode::InternalServerError))
}
async fn execute_commands(config: &Yml, agents: &Vec<Agent>) -> Result<(), tide::Error> {
debug!("working {:?}", config);
for agent in agents {
debug!("agent: {:?}", agent);
if agent.can_meet(&config.needs) {
debug!("agent: {:?} can meet our needs", agent);
let commands: Vec<synchronik::Command> = config
.commands
.iter()
.map(|c| synchronik::Command::with_script(c))
.collect();
let commands = synchronik::CommandRequest { commands };
let client = reqwest::Client::new();
let _res = client
.put(
agent
.url
.join("/api/v1/execute")
.expect("Failed to join execute URL"),
)
.json(&commands)
.send()
.await?;
}
}
Ok(())
}
}

View File

@ -50,7 +50,7 @@
<td>
<form method="POST" action="/api/v1/projects/{{this.name}}">
<input type="hidden" name="next" value="/project/{{this.name}}"/>
<input type="image" value="Execute" src="/static/icons/actions/view-refresh.svg"/>
<input type="image" title="Execute" value="Execute" src="/static/icons/actions/view-refresh.svg"/>
</form>
</td>
</tr>

View File

@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title>Janky - {{name}}</title>
<title>Synchronik - {{name}}</title>
<link type="text/css" rel="stylesheet" href="/static/bootstrap.min.css"/>
<script src="/static/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>