feat: add basic plan support
This commit is contained in:
parent
a4565c7c12
commit
040f1a8a56
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
.cuddle/
|
.cuddle/
|
||||||
|
.forest/
|
||||||
|
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -244,6 +244,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -773,6 +774,15 @@ version = "1.0.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -1122,6 +1132,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
@ -1153,6 +1173,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -16,3 +16,4 @@ uuid.workspace = true
|
|||||||
rusty-s3 = "0.7.0"
|
rusty-s3 = "0.7.0"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
kdl = "6.3.3"
|
kdl = "6.3.3"
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use kdl::{KdlDocument, KdlNode, KdlValue};
|
use kdl::KdlDocument;
|
||||||
use rusty_s3::{Bucket, Credentials, S3Action};
|
use rusty_s3::{Bucket, Credentials, S3Action};
|
||||||
|
|
||||||
use crate::state::SharedState;
|
use crate::{model::Project, plan_reconciler::PlanReconciler, state::SharedState};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
||||||
@ -45,80 +45,6 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ProjectPlan {
|
|
||||||
Local { path: PathBuf },
|
|
||||||
NoPlan,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&KdlNode> for ProjectPlan {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
|
||||||
let Some(children) = value.children() else {
|
|
||||||
return Ok(Self::NoPlan);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(local) = children.get_arg("local") {
|
|
||||||
return Ok(Self::Local {
|
|
||||||
path: local
|
|
||||||
.as_string()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.ok_or(anyhow::anyhow!("local must have an arg with a valid path"))?
|
|
||||||
.into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::NoPlan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Project {
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
plan: Option<ProjectPlan>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<KdlDocument> for Project {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
|
||||||
let project_section = value.get("project").ok_or(anyhow::anyhow!(
|
|
||||||
"forest.kdl project file must have a project object"
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let project_children = project_section
|
|
||||||
.children()
|
|
||||||
.ok_or(anyhow::anyhow!("a forest project must have children"))?;
|
|
||||||
|
|
||||||
let project_plan: Option<ProjectPlan> = if let Some(project) = project_children.get("plan")
|
|
||||||
{
|
|
||||||
Some(project.try_into()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name: project_children
|
|
||||||
.get_arg("name")
|
|
||||||
.and_then(|n| match n {
|
|
||||||
KdlValue::String(s) => Some(s),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.ok_or(anyhow::anyhow!("a forest kuddle project must have a name"))?,
|
|
||||||
description: project_children
|
|
||||||
.get_arg("description")
|
|
||||||
.and_then(|n| match n {
|
|
||||||
KdlValue::String(s) => Some(s.trim().to_string()),
|
|
||||||
_ => None,
|
|
||||||
}),
|
|
||||||
plan: project_plan,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute() -> anyhow::Result<()> {
|
pub async fn execute() -> anyhow::Result<()> {
|
||||||
let cli = Command::parse();
|
let cli = Command::parse();
|
||||||
|
|
||||||
@ -134,12 +60,15 @@ pub async fn execute() -> anyhow::Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let project_file = tokio::fs::read_to_string(project_file_path).await?;
|
let project_file = tokio::fs::read_to_string(&project_file_path).await?;
|
||||||
let project_doc: KdlDocument = project_file.parse()?;
|
let project_doc: KdlDocument = project_file.parse()?;
|
||||||
|
|
||||||
let project: Project = project_doc.try_into()?;
|
let project: Project = project_doc.try_into()?;
|
||||||
|
|
||||||
tracing::trace!("found a project name: {}, {:?}", project.name, project);
|
tracing::trace!("found a project name: {}, {:?}", project.name, project);
|
||||||
|
|
||||||
|
PlanReconciler::new()
|
||||||
|
.reconcile(&project, &project_path)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Serve {
|
Commands::Serve {
|
||||||
@ -162,7 +91,6 @@ pub async fn execute() -> anyhow::Result<()> {
|
|||||||
let _url = put_object.sign(std::time::Duration::from_secs(30));
|
let _url = put_object.sign(std::time::Duration::from_secs(30));
|
||||||
let _state = SharedState::new().await?;
|
let _state = SharedState::new().await?;
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod model;
|
||||||
|
pub mod plan_reconciler;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
77
crates/forest/src/model.rs
Normal file
77
crates/forest/src/model.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use kdl::{KdlDocument, KdlNode, KdlValue};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ProjectPlan {
|
||||||
|
Local { path: PathBuf },
|
||||||
|
NoPlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&KdlNode> for ProjectPlan {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
||||||
|
let Some(children) = value.children() else {
|
||||||
|
return Ok(Self::NoPlan);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(local) = children.get_arg("local") {
|
||||||
|
return Ok(Self::Local {
|
||||||
|
path: local
|
||||||
|
.as_string()
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("local must have an arg with a valid path"))?
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self::NoPlan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Project {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub plan: Option<ProjectPlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<KdlDocument> for Project {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
||||||
|
let project_section = value.get("project").ok_or(anyhow::anyhow!(
|
||||||
|
"forest.kdl project file must have a project object"
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let project_children = project_section
|
||||||
|
.children()
|
||||||
|
.ok_or(anyhow::anyhow!("a forest project must have children"))?;
|
||||||
|
|
||||||
|
let project_plan: Option<ProjectPlan> = if let Some(project) = project_children.get("plan")
|
||||||
|
{
|
||||||
|
Some(project.try_into()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: project_children
|
||||||
|
.get_arg("name")
|
||||||
|
.and_then(|n| match n {
|
||||||
|
KdlValue::String(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow::anyhow!("a forest kuddle project must have a name"))?,
|
||||||
|
description: project_children
|
||||||
|
.get_arg("description")
|
||||||
|
.and_then(|n| match n {
|
||||||
|
KdlValue::String(s) => Some(s.trim().to_string()),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
plan: project_plan,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
84
crates/forest/src/plan_reconciler.rs
Normal file
84
crates/forest/src/plan_reconciler.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use crate::model::Project;
|
||||||
|
|
||||||
|
pub mod local {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
pub async fn reconcile(source: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||||
|
for entry in walkdir::WalkDir::new(source) {
|
||||||
|
let entry = entry?;
|
||||||
|
let rel = entry.path().strip_prefix(source)?;
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
|
||||||
|
if metadata.is_file() {
|
||||||
|
tracing::trace!("copying file: {}", rel.display());
|
||||||
|
let dest_path = dest.join(rel);
|
||||||
|
|
||||||
|
tokio::fs::copy(entry.path(), &dest_path)
|
||||||
|
.await
|
||||||
|
.context(anyhow::anyhow!(
|
||||||
|
"failed to file directory at: {}",
|
||||||
|
dest_path.display()
|
||||||
|
))?;
|
||||||
|
} else if metadata.is_dir() {
|
||||||
|
let dest_path = dest.join(rel);
|
||||||
|
|
||||||
|
tracing::trace!("creating directory: {}", dest_path.display());
|
||||||
|
tokio::fs::create_dir_all(&dest_path)
|
||||||
|
.await
|
||||||
|
.context(anyhow::anyhow!(
|
||||||
|
"failed to create directory at: {}",
|
||||||
|
dest_path.display()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PlanReconciler {}
|
||||||
|
|
||||||
|
impl PlanReconciler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reconcile(&self, project: &Project, destination: &Path) -> anyhow::Result<()> {
|
||||||
|
tracing::info!("reconciling project");
|
||||||
|
if project.plan.is_none() {
|
||||||
|
tracing::debug!("no plan, returning");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the plan dir
|
||||||
|
// TODO: We're always deleting, consider some form of caching
|
||||||
|
let plan_dir = destination.join(".forest").join("plan");
|
||||||
|
if plan_dir.exists() {
|
||||||
|
tokio::fs::remove_dir_all(&plan_dir).await?;
|
||||||
|
}
|
||||||
|
tokio::fs::create_dir_all(&plan_dir)
|
||||||
|
.await
|
||||||
|
.context(anyhow::anyhow!(
|
||||||
|
"failed to create plan dir: {}",
|
||||||
|
plan_dir.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
match project.plan.as_ref().unwrap() {
|
||||||
|
crate::model::ProjectPlan::Local { path } => {
|
||||||
|
let source = &destination.join(path);
|
||||||
|
local::reconcile(source, &plan_dir).await?;
|
||||||
|
}
|
||||||
|
crate::model::ProjectPlan::NoPlan => {
|
||||||
|
tracing::debug!("no plan, returning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
3
examples/plan/forest.kdl
Normal file
3
examples/plan/forest.kdl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
plan {
|
||||||
|
name project
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user