use std::path::Path;
use rigg_core::resources::ResourceKind;
use crate::azure::rigg::plan::{PlanReport, ResourceRef, RiggApiAdapter};
#[derive(Debug, Default)]
pub struct PushOutcome {
pub created: Vec<ResourceRef>,
pub updated: Vec<ResourceRef>,
pub deleted: Vec<ResourceRef>,
}
#[derive(Debug, thiserror::Error)]
pub enum PushError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("yaml: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("rigg: {0}")]
Rigg(String),
}
const CREATE_ORDER: &[ResourceKind] = &[
ResourceKind::DataSource,
ResourceKind::Skillset,
ResourceKind::Index,
ResourceKind::Indexer,
ResourceKind::KnowledgeSource,
ResourceKind::KnowledgeBase,
];
pub async fn run<A: RiggApiAdapter>(
plan: PlanReport,
rigg_dir: &Path,
api: &A,
) -> Result<PushOutcome, PushError> {
let mut outcome = PushOutcome::default();
let mut creates_by_kind: std::collections::HashMap<ResourceKind, Vec<ResourceRef>> =
std::collections::HashMap::new();
let mut updates_by_kind: std::collections::HashMap<ResourceKind, Vec<ResourceRef>> =
std::collections::HashMap::new();
for rref in plan.creates {
creates_by_kind.entry(rref.kind).or_default().push(rref);
}
for (rref, _diff) in plan.updates {
updates_by_kind.entry(rref.kind).or_default().push(rref);
}
for kind in CREATE_ORDER {
if let Some(refs) = creates_by_kind.remove(kind) {
for rref in refs {
let body = read_resource_body(rigg_dir, &rref)?;
api.upsert_resource(rref.kind, &rref.name, &body)
.await
.map_err(|e| PushError::Rigg(e.to_string()))?;
outcome.created.push(rref);
}
}
if let Some(refs) = updates_by_kind.remove(kind) {
for rref in refs {
let body = read_resource_body(rigg_dir, &rref)?;
api.upsert_resource(rref.kind, &rref.name, &body)
.await
.map_err(|e| PushError::Rigg(e.to_string()))?;
outcome.updated.push(rref);
}
}
}
let mut deletes_by_kind: std::collections::HashMap<ResourceKind, Vec<ResourceRef>> =
std::collections::HashMap::new();
for rref in plan.deletes {
deletes_by_kind.entry(rref.kind).or_default().push(rref);
}
for kind in CREATE_ORDER.iter().rev() {
if let Some(refs) = deletes_by_kind.remove(kind) {
for rref in refs {
api.delete_resource(rref.kind, &rref.name)
.await
.map_err(|e| PushError::Rigg(e.to_string()))?;
outcome.deleted.push(rref);
}
}
}
Ok(outcome)
}
fn read_resource_body(rigg_dir: &Path, rref: &ResourceRef) -> Result<serde_json::Value, PushError> {
let subdir = crate::azure::rigg::plan::subdir_for_kind(rref.kind);
let path = rigg_dir.join(subdir).join(format!("{}.yaml", rref.name));
let yaml_text = std::fs::read_to_string(&path)?;
let yaml_val: serde_yaml::Value = serde_yaml::from_str(&yaml_text)?;
let json_str = serde_json::to_string(&yaml_val).map_err(|e| PushError::Rigg(e.to_string()))?;
let json_val: serde_json::Value =
serde_json::from_str(&json_str).map_err(|e| PushError::Rigg(e.to_string()))?;
Ok(json_val)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::azure::rigg::plan::subdir_for_kind;
use crate::azure::rigg::plan::tests::MockRiggApi;
use std::path::Path;
fn write_resource(dir: &Path, kind: ResourceKind, name: &str) {
let subdir = dir.join(subdir_for_kind(kind));
std::fs::create_dir_all(&subdir).unwrap();
let content = format!("name: {name}\n");
std::fs::write(subdir.join(format!("{name}.yaml")), content).unwrap();
}
fn make_plan(
creates: Vec<(ResourceKind, &str)>,
updates: Vec<(ResourceKind, &str)>,
deletes: Vec<(ResourceKind, &str)>,
) -> PlanReport {
PlanReport {
creates: creates
.into_iter()
.map(|(k, n)| ResourceRef {
kind: k,
name: n.to_string(),
})
.collect(),
updates: updates
.into_iter()
.map(|(k, n)| {
(
ResourceRef {
kind: k,
name: n.to_string(),
},
crate::azure::rigg::plan::ResourceDiff {
field_changes: vec![],
},
)
})
.collect(),
deletes: deletes
.into_iter()
.map(|(k, n)| ResourceRef {
kind: k,
name: n.to_string(),
})
.collect(),
unchanged: vec![],
}
}
#[tokio::test]
async fn push_creates_in_dependency_order() {
let dir = tempfile::tempdir().unwrap();
write_resource(dir.path(), ResourceKind::Index, "my-index");
write_resource(dir.path(), ResourceKind::DataSource, "my-ds");
write_resource(dir.path(), ResourceKind::Skillset, "my-skillset");
write_resource(dir.path(), ResourceKind::Indexer, "my-indexer");
write_resource(dir.path(), ResourceKind::KnowledgeSource, "my-ks");
write_resource(dir.path(), ResourceKind::KnowledgeBase, "my-kb");
let plan = make_plan(
vec![
(ResourceKind::KnowledgeBase, "my-kb"),
(ResourceKind::Index, "my-index"),
(ResourceKind::Indexer, "my-indexer"),
(ResourceKind::DataSource, "my-ds"),
(ResourceKind::Skillset, "my-skillset"),
(ResourceKind::KnowledgeSource, "my-ks"),
],
vec![],
vec![],
);
let api = MockRiggApi::default();
let outcome = run(plan, dir.path(), &api).await.unwrap();
let upserted = api.upserted.lock().unwrap();
let kinds: Vec<ResourceKind> = upserted.iter().map(|(k, _)| *k).collect();
let pos = |target: ResourceKind| kinds.iter().position(|k| *k == target).unwrap();
assert!(pos(ResourceKind::DataSource) < pos(ResourceKind::Skillset));
assert!(pos(ResourceKind::Skillset) < pos(ResourceKind::Index));
assert!(pos(ResourceKind::Index) < pos(ResourceKind::Indexer));
assert!(pos(ResourceKind::Indexer) < pos(ResourceKind::KnowledgeSource));
assert!(pos(ResourceKind::KnowledgeSource) < pos(ResourceKind::KnowledgeBase));
assert_eq!(outcome.created.len(), 6);
assert!(outcome.updated.is_empty());
assert!(outcome.deleted.is_empty());
}
#[tokio::test]
async fn push_deletes_in_reverse_order() {
let dir = tempfile::tempdir().unwrap();
let plan = make_plan(
vec![],
vec![],
vec![
(ResourceKind::DataSource, "ds"),
(ResourceKind::Index, "idx"),
(ResourceKind::KnowledgeBase, "kb"),
],
);
let api = MockRiggApi::default();
let outcome = run(plan, dir.path(), &api).await.unwrap();
let deleted = api.deleted.lock().unwrap();
let kinds: Vec<ResourceKind> = deleted.iter().map(|(k, _)| *k).collect();
let pos = |target: ResourceKind| kinds.iter().position(|k| *k == target).unwrap();
assert!(pos(ResourceKind::KnowledgeBase) < pos(ResourceKind::Index));
assert!(pos(ResourceKind::Index) < pos(ResourceKind::DataSource));
assert_eq!(outcome.deleted.len(), 3);
assert!(outcome.created.is_empty());
assert!(outcome.updated.is_empty());
}
#[tokio::test]
async fn push_updates_in_dependency_order() {
let dir = tempfile::tempdir().unwrap();
write_resource(dir.path(), ResourceKind::DataSource, "ds");
write_resource(dir.path(), ResourceKind::Indexer, "idx");
let plan = make_plan(
vec![],
vec![
(ResourceKind::Indexer, "idx"),
(ResourceKind::DataSource, "ds"),
],
vec![],
);
let api = MockRiggApi::default();
let outcome = run(plan, dir.path(), &api).await.unwrap();
let upserted = api.upserted.lock().unwrap();
let kinds: Vec<ResourceKind> = upserted.iter().map(|(k, _)| *k).collect();
let pos = |target: ResourceKind| kinds.iter().position(|k| *k == target).unwrap();
assert!(pos(ResourceKind::DataSource) < pos(ResourceKind::Indexer));
assert_eq!(outcome.updated.len(), 2);
}
}