feat: make sure dir is there as well

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-01-28 16:41:50 +01:00
parent cc7aaf14eb
commit 85cc1d46db
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
15 changed files with 1153 additions and 447 deletions

741
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,7 @@ async fn dind_image(
"libz-dev", "libz-dev",
]) ])
.with_workdir("/app/cuddle/") .with_workdir("/app/cuddle/")
.with_directory(".", src.id().await?) .with_directory(".", src)
.with_exec(vec![ .with_exec(vec![
"cargo", "cargo",
"install", "install",
@ -99,7 +99,7 @@ async fn dind_image(
.from("docker:dind") .from("docker:dind")
.with_directory( .with_directory(
"/usr/local/cargo/bin/", "/usr/local/cargo/bin/",
rust_bin.directory("/usr/local/cargo/bin/").id().await?, rust_bin.directory("/usr/local/cargo/bin/"),
); );
let path_env = final_image.env_variable("PATH").await?; let path_env = final_image.env_variable("PATH").await?;

View File

@ -40,3 +40,12 @@ serde_json = "1.0.112"
rlua = "0.19.8" rlua = "0.19.8"
rlua-searcher = "0.1.0" rlua-searcher = "0.1.0"
dotenv = { version = "0.15.0", features = ["clap"] } dotenv = { version = "0.15.0", features = ["clap"] }
blake3 = "1.5.0"
tokio = { version = "1.35.1", features = ["full"] }
futures-util = "0.3.30"
fs_extra = "1.3.0"
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["rustls-tls", "json"]

View File

@ -78,7 +78,7 @@ impl CuddleAction {
log::trace!("preparing to run action"); log::trace!("preparing to run action");
return match ShellAction::new( match ShellAction::new(
self.name.clone(), self.name.clone(),
format!( format!(
"{}/scripts/{}.sh", "{}/scripts/{}.sh",
@ -98,7 +98,7 @@ impl CuddleAction {
log::error!("{}", e); log::error!("{}", e);
Err(e) Err(e)
} }
}; }
} }
CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")), CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")),
CuddleScript::Lua(l) => { CuddleScript::Lua(l) => {
@ -183,6 +183,184 @@ impl CuddleAction {
Ok(()) Ok(())
} }
CuddleScript::Rust(script) => Ok(()),
}
}
}
pub mod rust_action {
use std::{path::PathBuf, time::Duration};
use anyhow::Context;
use futures_util::StreamExt;
use reqwest::Method;
use tokio::{fs::File, io::AsyncWriteExt};
use crate::model::{CuddleRustScript, CuddleRustUpstream, CuddleVariable};
pub struct RustActionConfig {
pub config_dir: PathBuf,
pub cache_dir: PathBuf,
}
impl Default for RustActionConfig {
fn default() -> Self {
let config = dirs::config_dir().expect("to be able to find a valid .config dir");
let cache = dirs::cache_dir().expect("to be able to find a valid .cache dir");
Self {
config_dir: config,
cache_dir: cache,
}
}
}
pub struct RustAction {
pub config: RustActionConfig,
pub plan: String,
pub binary_name: String,
}
impl RustAction {
pub fn new(plan: String, binary_name: String) -> Self {
Self {
plan,
binary_name,
config: RustActionConfig::default(),
}
}
pub async fn execute(
&self,
script: CuddleRustScript,
variables: impl IntoIterator<Item = CuddleVariable>,
) -> anyhow::Result<()> {
let commit_sha = self
.get_commit_sha()
.await
.context("failed to find a valid commit sha on the inferred path: .cuddle/plan")?;
let binary_hash = self.calculate_hash(commit_sha)?;
// Get cached binary
// let binary = match self.get_binary(&binary_hash).await? {
// Some(binary) => binary,
// None => self.fetch_binary(&script, &binary_hash).await?,
// };
// Execute binary
Ok(())
}
async fn get_binary(
&self,
binary_hash: impl Into<String>,
) -> anyhow::Result<Option<RustBinary>> {
let binary_path = self.get_cached_binary_path(binary_hash);
if !binary_path.exists() {
return Ok(None);
}
Ok(Some(RustBinary {}))
}
fn get_cached_binary_path(&self, binary_hash: impl Into<String>) -> PathBuf {
let cached_binary_name = self.get_cached_binary_name(binary_hash);
let binary_path = self
.config
.cache_dir
.join("binaries")
.join(cached_binary_name);
binary_path
}
#[inline]
fn get_cached_binary_name(&self, binary_hash: impl Into<String>) -> String {
format!("{}-{}", binary_hash.into(), self.binary_name)
}
async fn get_commit_sha(&self) -> anyhow::Result<String> {
let repo = git2::Repository::open(".cuddle/plan")?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
let commit_sha = commit.id();
Ok(commit_sha.to_string())
}
// async fn fetch_binary(
// &self,
// script: &CuddleRustScript,
// binary_hash: impl Into<String>,
// ) -> anyhow::Result<RustBinary> {
//let upstream = &script.upstream;
//TODO: we should interpret some template variables in the upstream string. Ignore for now though
// match UpstreamRustBinary::from(upstream) {
// UpstreamRustBinary::HttpBased { url } => {
// let client = reqwest::ClientBuilder::new()
// .user_agent(concat!(
// env!("CARGO_PKG_NAME"),
// "/",
// env!("CARGO_PKG_VERSION")
// ))
// .connect_timeout(Duration::from_secs(5))
// .build()?;
// let resp = client.request(Method::GET, url).send().await?;
// let mut stream = resp.bytes_stream();
// let mut file = File::create(self.get_cached_binary_name(binary_hash)).await?;
// while let Some(item) = stream.next().await {
// let chunk = item?;
// file.write_all(&chunk).await?;
// }
// // Make sure the entire file is written before we execute it
// file.flush().await?;
// todo!()
// }
// }
// }
fn calculate_hash(&self, commit_sha: impl Into<Vec<u8>>) -> anyhow::Result<String> {
let mut contents: Vec<u8> = Vec::new();
contents.append(&mut self.plan.clone().into_bytes());
contents.append(&mut commit_sha.into());
let hash = blake3::hash(&contents);
let hex = hash.to_hex();
Ok(hex.to_string())
}
}
pub struct RustBinary {}
pub enum UpstreamRustBinary {
HttpBased { url: String },
}
impl From<CuddleRustUpstream> for UpstreamRustBinary {
fn from(value: CuddleRustUpstream) -> Self {
match value {
CuddleRustUpstream::Gitea { url } => Self::HttpBased { url },
}
}
}
impl From<&CuddleRustUpstream> for UpstreamRustBinary {
fn from(value: &CuddleRustUpstream) -> Self {
match value {
CuddleRustUpstream::Gitea { url } => Self::HttpBased { url: url.clone() },
}
} }
} }
} }

View File

@ -15,7 +15,10 @@ use crate::{
util::git::GitCommit, util::git::GitCommit,
}; };
use self::subcommands::render_template::RenderTemplateCommand; use self::subcommands::{
render::RenderCommand, render_kustomize::RenderKustomizeCommand,
render_template::RenderTemplateCommand,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CuddleCli { pub struct CuddleCli {
@ -73,9 +76,8 @@ impl CuddleCli {
if let Ok(context_iter) = self.context.clone().unwrap().lock() { if let Ok(context_iter) = self.context.clone().unwrap().lock() {
for ctx in context_iter.iter() { for ctx in context_iter.iter() {
if let Some(variables) = ctx.plan.vars.clone() { if let Some(variables) = ctx.plan.vars.clone() {
for (name, var) in variables { let mut variables: Vec<CuddleVariable> = variables.into();
self.variables.push(CuddleVariable::new(name, var)) self.variables.append(&mut variables);
}
} }
if let CuddleTreeType::Root = ctx.node_type { if let CuddleTreeType::Root = ctx.node_type {
@ -83,7 +85,7 @@ impl CuddleCli {
temp_path.push(".cuddle/tmp"); temp_path.push(".cuddle/tmp");
self.variables.push(CuddleVariable::new( self.variables.push(CuddleVariable::new(
"tmp".into(), "tmp",
temp_path.clone().to_string_lossy().to_string(), temp_path.clone().to_string_lossy().to_string(),
)); ));
@ -93,10 +95,9 @@ impl CuddleCli {
} }
match GitCommit::new() { match GitCommit::new() {
Ok(commit) => self.variables.push(CuddleVariable::new( Ok(commit) => self
"commit_sha".into(), .variables
commit.commit_sha.clone(), .push(CuddleVariable::new("commit_sha", commit.commit_sha.clone())),
)),
Err(e) => { Err(e) => {
log::debug!("{}", e); log::debug!("{}", e);
} }
@ -126,6 +127,7 @@ impl CuddleCli {
name, name,
l.description.clone(), l.description.clone(),
)), )),
CuddleScript::Rust(_) => todo!(),
} }
} }
} }
@ -136,7 +138,7 @@ impl CuddleCli {
} }
fn process_templates(self) -> anyhow::Result<Self> { fn process_templates(self) -> anyhow::Result<Self> {
if let None = self.tmp_dir { if self.tmp_dir.is_none() {
log::debug!("cannot process template as bare bones cli"); log::debug!("cannot process template as bare bones cli");
return Ok(self); return Ok(self);
} }
@ -147,10 +149,8 @@ impl CuddleCli {
.clone() .clone()
.ok_or(anyhow::anyhow!("tmp_dir does not exist aborting"))?; .ok_or(anyhow::anyhow!("tmp_dir does not exist aborting"))?;
match self.config.get_fetch_policy()? { match self.config.get_fetch_policy()? {
CuddleFetchPolicy::Always => { CuddleFetchPolicy::Always if tmp_dir.exists() && tmp_dir.ends_with("tmp") => {
if tmp_dir.exists() && tmp_dir.ends_with("tmp") { std::fs::remove_dir_all(tmp_dir.clone())?;
std::fs::remove_dir_all(tmp_dir.clone())?;
}
} }
_ => {} _ => {}
} }
@ -169,12 +169,30 @@ impl CuddleCli {
continue; continue;
} }
for file in std::fs::read_dir(template_path)?.into_iter() { for file in std::fs::read_dir(&template_path)? {
let f = file?; let f = file?;
let mut dest_file = tmp_dir.clone(); let mut dest_file = tmp_dir.clone();
dest_file.push(f.file_name()); dest_file.push(f.path().strip_prefix(&template_path)?.parent().unwrap());
std::fs::copy(f.path(), dest_file)?; tracing::trace!(
"moving from: {} to {}",
f.path().display(),
dest_file.display()
);
if f.path().is_dir() {
std::fs::create_dir_all(&dest_file)?;
}
fs_extra::copy_items(
&[f.path()],
&dest_file,
&fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
..Default::default()
},
)?;
} }
} }
} }
@ -194,6 +212,8 @@ impl CuddleCli {
root_cmd = subcommands::x::build_command(root_cmd, self.clone()); root_cmd = subcommands::x::build_command(root_cmd, self.clone());
root_cmd = subcommands::render_template::build_command(root_cmd); root_cmd = subcommands::render_template::build_command(root_cmd);
root_cmd = subcommands::render_kustomize::build_command(root_cmd);
root_cmd = subcommands::render::build_command(root_cmd);
root_cmd = subcommands::init::build_command(root_cmd, self.clone()); root_cmd = subcommands::init::build_command(root_cmd, self.clone());
self.command = Some(root_cmd); self.command = Some(root_cmd);
@ -223,11 +243,20 @@ impl CuddleCli {
let res = match matches.subcommand() { let res = match matches.subcommand() {
Some(("x", exe_submatch)) => subcommands::x::execute_x(exe_submatch, self.clone()), Some(("x", exe_submatch)) => subcommands::x::execute_x(exe_submatch, self.clone()),
Some(("render", sub_matches)) => {
RenderCommand::execute(sub_matches, self.clone())?;
Ok(())
}
Some(("render_template", sub_matches)) => { Some(("render_template", sub_matches)) => {
RenderTemplateCommand::from_matches(sub_matches, self.clone()) RenderTemplateCommand::from_matches(sub_matches, self.clone())
.and_then(|cmd| cmd.execute())?; .and_then(|cmd| cmd.execute())?;
Ok(()) Ok(())
} }
Some(("render-kustomize", sub_matches)) => {
RenderKustomizeCommand::from_matches(sub_matches, self.clone())
.and_then(|cmd| cmd.execute())?;
Ok(())
}
Some(("init", sub_matches)) => { Some(("init", sub_matches)) => {
subcommands::init::execute_init(sub_matches, self.clone()) subcommands::init::execute_init(sub_matches, self.clone())
} }

View File

@ -0,0 +1,168 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::Context;
use clap::{Arg, ArgAction, ArgMatches, Command};
use serde_json::from_value;
use tera::Function;
use crate::{cli::CuddleCli, model::CuddleVariable};
pub fn build_command(root_cmd: Command) -> Command {
root_cmd.subcommand(
Command::new("folder")
.about("renders a template folder")
.args(&[
Arg::new("source")
.long("source")
.required(true)
.value_parser(clap::value_parser!(PathBuf)),
Arg::new("destination")
.long("destination")
.required(true)
.value_parser(clap::value_parser!(PathBuf)),
Arg::new("extra-var")
.long("extra-var")
.action(ArgAction::Append)
.required(false),
]),
)
}
pub struct FolderCommand {
variables: Vec<CuddleVariable>,
source: PathBuf,
destination: PathBuf,
}
impl FolderCommand {
pub fn from_matches(matches: &ArgMatches, cli: CuddleCli) -> anyhow::Result<Self> {
let source = matches
.get_one::<PathBuf>("source")
.expect("source")
.clone();
let destination = matches
.get_one::<PathBuf>("destination")
.expect("destination")
.clone();
let mut extra_vars: Vec<CuddleVariable> =
if let Some(extra_vars) = matches.get_many::<String>("extra-var") {
let mut vars = Vec::with_capacity(extra_vars.len());
for var in extra_vars.into_iter() {
let parts: Vec<&str> = var.split('=').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("extra-var: is not set correctly: {}", var));
}
vars.push(CuddleVariable::new(parts[0], parts[1]));
}
vars
} else {
vec![]
};
extra_vars.append(&mut cli.variables.clone());
Ok(Self {
variables: extra_vars,
source,
destination,
})
}
pub fn execute(self) -> anyhow::Result<()> {
std::fs::remove_dir_all(&self.destination)?;
std::fs::create_dir_all(&self.destination)?;
// Prepare context
let mut context = tera::Context::new();
for var in &self.variables {
context.insert(var.name.to_lowercase().replace([' ', '-'], "_"), &var.value)
}
let mut tera = tera::Tera::default();
tera.register_function("filter_by_prefix", filter_by_prefix(self.variables.clone()));
for entry in walkdir::WalkDir::new(&self.source) {
let entry = entry?;
let entry_path = entry.path();
let rel_path = self
.destination
.join(entry_path.strip_prefix(&self.source)?);
if entry_path.is_file() {
// Load source template
let source = std::fs::read_to_string(entry_path)?;
let output = tera.render_str(&source, &context)?;
if let Some(parent) = rel_path.parent() {
std::fs::create_dir_all(parent)?;
}
// Put template in final destination
std::fs::write(&rel_path, output).context(format!(
"failed to write to destination: {}",
&rel_path.display()
))?;
log::info!("finished writing template to: {}", &rel_path.display());
}
}
Ok(())
}
}
fn filter_by_prefix(variables: Vec<CuddleVariable>) -> impl Function {
Box::new(
move |args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
for var in &variables {
tracing::info!("variable: {} - {}", var.name, var.value);
}
let prefix = match args.get("prefix") {
Some(value) => match from_value::<Vec<String>>(value.clone()) {
Ok(prefix) => prefix,
Err(e) => {
tracing::error!("prefix was not a string: {}", e);
return Err("prefix was not a string".into());
}
},
None => return Err("prefix is required".into()),
};
let prefix = prefix.join("_");
let vars = variables
.iter()
.filter_map(|v| {
if v.name.starts_with(&prefix) {
Some(CuddleVariable::new(
v.name.trim_start_matches(&prefix).trim_start_matches('_'),
v.value.clone(),
))
} else {
None
}
})
.collect::<Vec<CuddleVariable>>();
tracing::info!("was here");
let mut structure: HashMap<String, String> = HashMap::new();
for var in vars {
tracing::info!("found: {} - {}", &var.name, &var.value);
structure.insert(var.name, var.value);
}
Ok(serde_json::to_value(structure).unwrap())
},
)
}
fn filter_by_name(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
Ok(tera::Value::Null)
}

View File

@ -0,0 +1,85 @@
use std::{io::Write, path::PathBuf};
use clap::{Arg, ArgMatches, Command};
use crate::cli::CuddleCli;
pub fn build_command(root_cmd: Command) -> Command {
root_cmd.subcommand(
Command::new("kustomize")
.about("renders a kustomize folder")
.args(&[
Arg::new("kustomize-folder")
.long("kustomize-folder")
.value_parser(clap::value_parser!(PathBuf))
.required(true),
Arg::new("destination")
.long("destination")
.required(true)
.value_parser(clap::value_parser!(PathBuf)),
]),
)
}
pub struct KustomizeCommand {
kustomize_folder: PathBuf,
destination: PathBuf,
}
impl KustomizeCommand {
pub fn from_matches(matches: &ArgMatches, _cli: CuddleCli) -> anyhow::Result<Self> {
let kustomize_folder = matches
.get_one::<PathBuf>("kustomize-folder")
.expect("kustomize-folder")
.clone();
let destination = matches
.get_one::<PathBuf>("destination")
.expect("destination")
.clone();
Ok(Self {
kustomize_folder,
destination,
})
}
pub fn execute(self) -> anyhow::Result<()> {
let mut cmd = std::process::Command::new("kubectl");
std::fs::remove_dir_all(&self.destination)?;
std::fs::create_dir_all(&self.destination)?;
let cmd = cmd.arg("kustomize").arg(self.kustomize_folder);
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"failed to run kustomize: {}",
output.status.code().expect("to find exit code")
)
}
let mut cmd = std::process::Command::new("kubectl-slice");
let cmd = cmd
.arg("-o")
.arg(self.destination)
.stdin(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(&output.stdout)?;
}
let output = child.wait_with_output()?;
if !output.status.success() {
anyhow::bail!(
"failed to run kustomize: {}",
output.status.code().expect("to find exit code")
)
}
Ok(())
}
}

View File

@ -1,3 +1,7 @@
pub mod folder;
pub mod init; pub mod init;
pub mod kustomize;
pub mod render;
pub mod render_kustomize;
pub mod render_template; pub mod render_template;
pub mod x; pub mod x;

View File

@ -0,0 +1,31 @@
use clap::{ArgMatches, Command};
use crate::cli::CuddleCli;
use super::{folder::FolderCommand, kustomize::KustomizeCommand};
pub fn build_command(root_cmd: Command) -> Command {
let cmd = Command::new("render").about("accesses different render commands");
let cmd = super::kustomize::build_command(cmd);
let cmd = super::folder::build_command(cmd);
root_cmd.subcommand(cmd)
}
pub struct RenderCommand {}
impl RenderCommand {
pub fn execute(matches: &ArgMatches, cli: CuddleCli) -> anyhow::Result<()> {
match matches.subcommand() {
Some(("kustomize", sub_matches)) => {
KustomizeCommand::from_matches(sub_matches, cli)?.execute()?;
}
Some(("folder", sub_matches)) => {
FolderCommand::from_matches(sub_matches, cli)?.execute()?;
}
_ => anyhow::bail!("failed to find match for render"),
}
Ok(())
}
}

View File

@ -0,0 +1,70 @@
use std::path::PathBuf;
use clap::{Arg, ArgMatches, Command};
use crate::cli::CuddleCli;
pub fn build_command(root_cmd: Command) -> Command {
root_cmd.subcommand(
Command::new("render-kustomize")
.about("renders a kustomize folder")
.args(&[
Arg::new("kustomize-folder")
.long("kustomize-folder")
.value_parser(clap::value_parser!(PathBuf))
.required(true),
Arg::new("destination")
.long("destination")
.required(true)
.value_parser(clap::value_parser!(PathBuf)),
]),
)
}
pub struct RenderKustomizeCommand {
kustomize_folder: PathBuf,
destination: PathBuf,
}
impl RenderKustomizeCommand {
pub fn from_matches(matches: &ArgMatches, _cli: CuddleCli) -> anyhow::Result<Self> {
let kustomize_folder = matches
.get_one::<PathBuf>("kustomize-folder")
.expect("kustomize-folder")
.clone();
let destination = matches
.get_one::<PathBuf>("destination")
.expect("destination")
.clone();
Ok(Self {
kustomize_folder,
destination,
})
}
pub fn execute(self) -> anyhow::Result<()> {
let mut cmd = std::process::Command::new("kubectl");
std::fs::create_dir_all(&self.destination)?;
let cmd = cmd
.arg("kustomize")
.arg(self.kustomize_folder)
.arg(format!("--output={}", self.destination.display()));
let mut process = cmd.spawn()?;
let exit = process.wait()?;
if !exit.success() {
anyhow::bail!(
"failed to run kustomize: {}",
exit.code().expect("to find exit code")
)
}
Ok(())
}
}

View File

@ -1,11 +1,12 @@
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr};
use clap::{Arg, ArgGroup, ArgMatches, Command}; use anyhow::Context;
use clap::{Arg, ArgMatches, Command};
use crate::{cli::CuddleCli, model::CuddleVariable}; use crate::{cli::CuddleCli, model::CuddleVariable};
const DESTINATION: &'static str = "destination is the output path of the template once done, but default .tmpl is stripped and the normal file extension is used. this can be overwritten if a file path is entered instead. I.e. (/some/file/name.txt)"; const DESTINATION: &str = "destination is the output path of the template once done, but default .tmpl is stripped and the normal file extension is used. this can be overwritten if a file path is entered instead. I.e. (/some/file/name.txt)";
const TEMPLATE_FILE: &'static str = "template-file is the input file path of the .tmpl file (or inferred) that you would like to render"; const TEMPLATE_FILE: &str = "template-file is the input file path of the .tmpl file (or inferred) that you would like to render";
pub fn build_command(root_cmd: Command) -> Command { pub fn build_command(root_cmd: Command) -> Command {
root_cmd.subcommand( root_cmd.subcommand(
@ -51,18 +52,19 @@ impl RenderTemplateCommand {
.get_one::<String>("destination") .get_one::<String>("destination")
.ok_or(anyhow::anyhow!("destination was not found")) .ok_or(anyhow::anyhow!("destination was not found"))
.and_then(get_path_buf_and_check_dir_exists) .and_then(get_path_buf_and_check_dir_exists)
.and_then(RenderTemplateCommand::transform_extension)?; .and_then(RenderTemplateCommand::transform_extension)
.context("failed to access dest directory")?;
let mut extra_vars: Vec<CuddleVariable> = let mut extra_vars: Vec<CuddleVariable> =
if let Some(extra_vars) = matches.get_many::<String>("extra-var") { if let Some(extra_vars) = matches.get_many::<String>("extra-var") {
let mut vars = Vec::with_capacity(extra_vars.len()); let mut vars = Vec::with_capacity(extra_vars.len());
for var in extra_vars.into_iter() { for var in extra_vars.into_iter() {
let parts: Vec<&str> = var.split("=").collect(); let parts: Vec<&str> = var.split('=').collect();
if parts.len() != 2 { if parts.len() != 2 {
return Err(anyhow::anyhow!("extra-var: is not set correctly: {}", var)); return Err(anyhow::anyhow!("extra-var: is not set correctly: {}", var));
} }
vars.push(CuddleVariable::new(parts[0].into(), parts[1].into())); vars.push(CuddleVariable::new(parts[0], parts[1]));
} }
vars vars
} else { } else {
@ -82,10 +84,7 @@ impl RenderTemplateCommand {
// Prepare context // Prepare context
let mut context = tera::Context::new(); let mut context = tera::Context::new();
for var in self.variables { for var in self.variables {
context.insert( context.insert(var.name.to_lowercase().replace([' ', '-'], "_"), &var.value)
var.name.to_lowercase().replace(" ", "_").replace("-", "_"),
&var.value,
)
} }
// Load source template // Load source template
@ -93,8 +92,15 @@ impl RenderTemplateCommand {
let output = tera::Tera::one_off(source.as_str(), &context, false)?; let output = tera::Tera::one_off(source.as_str(), &context, false)?;
if let Some(parent) = self.destination.parent() {
std::fs::create_dir_all(parent)?;
}
// Put template in final destination // Put template in final destination
std::fs::write(&self.destination, output)?; std::fs::write(&self.destination, output).context(format!(
"failed to write to destination: {}",
&self.destination.display(),
))?;
log::info!( log::info!(
"finished writing template to: {}", "finished writing template to: {}",
@ -123,8 +129,8 @@ impl RenderTemplateCommand {
} }
} }
fn get_path_buf_and_check_exists(raw_path: &String) -> anyhow::Result<PathBuf> { fn get_path_buf_and_check_exists(raw_path: impl Into<String>) -> anyhow::Result<PathBuf> {
match PathBuf::from_str(&raw_path) { match PathBuf::from_str(&raw_path.into()) {
Ok(pb) => { Ok(pb) => {
if pb.exists() { if pb.exists() {
Ok(pb) Ok(pb)
@ -139,17 +145,9 @@ fn get_path_buf_and_check_exists(raw_path: &String) -> anyhow::Result<PathBuf> {
} }
} }
fn get_path_buf_and_check_dir_exists(raw_path: &String) -> anyhow::Result<PathBuf> { fn get_path_buf_and_check_dir_exists(raw_path: impl Into<String>) -> anyhow::Result<PathBuf> {
match PathBuf::from_str(&raw_path) { match PathBuf::from_str(&raw_path.into()) {
Ok(pb) => { Ok(pb) => Ok(pb),
if pb.is_dir() && pb.exists() {
Ok(pb)
} else if pb.is_file() {
Ok(pb)
} else {
Ok(pb)
}
}
Err(e) => Err(anyhow::anyhow!(e)), Err(e) => Err(anyhow::anyhow!(e)),
} }
} }

View File

@ -61,6 +61,7 @@ pub fn build_scripts(cli: CuddleCli) -> Vec<Command> {
} }
crate::model::CuddleScript::Dagger(_) => todo!(), crate::model::CuddleScript::Dagger(_) => todo!(),
crate::model::CuddleScript::Lua(l) => {} crate::model::CuddleScript::Lua(l) => {}
crate::model::CuddleScript::Rust(_) => todo!(),
} }
cmds.push(cmd) cmds.push(cmd)

View File

@ -1,12 +1,10 @@
use std::{ use std::{
env::{self, current_dir}, env::{self, current_dir},
ffi::OsStr, ffi::OsStr,
io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use anyhow::Context;
use git2::{ use git2::{
build::{CheckoutBuilder, RepoBuilder}, build::{CheckoutBuilder, RepoBuilder},
FetchOptions, RemoteCallbacks, FetchOptions, RemoteCallbacks,
@ -46,7 +44,7 @@ pub fn extract_cuddle(
// Load main cuddle file. // Load main cuddle file.
let cuddle_yaml = find_root_cuddle()?; let cuddle_yaml = find_root_cuddle()?;
if let None = cuddle_yaml { if cuddle_yaml.is_none() {
return Ok(None); return Ok(None);
} }
let cuddle_yaml = cuddle_yaml.unwrap(); let cuddle_yaml = cuddle_yaml.unwrap();
@ -122,7 +120,7 @@ fn pull_parent_cuddle_into_local(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut rc = RemoteCallbacks::new(); let mut rc = RemoteCallbacks::new();
rc.credentials(|_url, username_from_url, _allowed_types| { rc.credentials(|_url, username_from_url, _allowed_types| {
if "true".to_string() == std::env::var("CUDDLE_SSH_AGENT").ok().unwrap_or("".into()) { if *"true" == std::env::var("CUDDLE_SSH_AGENT").ok().unwrap_or("".into()) {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap()) git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
} else { } else {
git2::Cred::ssh_key( git2::Cred::ssh_key(
@ -155,7 +153,7 @@ fn recurse_parent(
context: Arc<Mutex<Vec<CuddleContext>>>, context: Arc<Mutex<Vec<CuddleContext>>>,
) -> anyhow::Result<Option<()>> { ) -> anyhow::Result<Option<()>> {
let cuddle_contents = find_cuddle(path.clone())?; let cuddle_contents = find_cuddle(path.clone())?;
if let None = cuddle_contents { if cuddle_contents.is_none() {
return Ok(None); return Ok(None);
} }
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents.unwrap())?; let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents.unwrap())?;
@ -172,14 +170,12 @@ fn recurse_parent(
} }
match cuddle_plan.base { match cuddle_plan.base {
CuddleBase::Bool(true) => { CuddleBase::Bool(true) => Err(anyhow::anyhow!(
return Err(anyhow::anyhow!( "plan cannot be enabled without specifying a plan"
"plan cannot be enabled without specifying a plan" )),
))
}
CuddleBase::Bool(false) => { CuddleBase::Bool(false) => {
log::debug!("plan is root: finishing up"); log::debug!("plan is root: finishing up");
return Ok(Some(())); Ok(Some(()))
} }
CuddleBase::String(parent_plan) => { CuddleBase::String(parent_plan) => {
let destination_path = create_cuddle(path.clone())?; let destination_path = create_cuddle(path.clone())?;
@ -189,7 +185,7 @@ fn recurse_parent(
if !cuddle_dest.exists() { if !cuddle_dest.exists() {
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?; pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
} }
return recurse_parent(cuddle_dest, context.clone()); recurse_parent(cuddle_dest, context.clone())
} }
} }
} }

View File

@ -47,6 +47,18 @@ pub struct CuddleLuaScript {
pub description: Option<String>, pub description: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum CuddleRustUpstream {
#[serde(alias = "gitea")]
Gitea { url: String },
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct CuddleRustScript {
pub description: Option<String>,
pub upstream: CuddleRustUpstream,
}
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum CuddleScript { pub enum CuddleScript {
@ -56,16 +68,66 @@ pub enum CuddleScript {
Dagger(CuddleDaggerScript), Dagger(CuddleDaggerScript),
#[serde(alias = "lua")] #[serde(alias = "lua")]
Lua(CuddleLuaScript), Lua(CuddleLuaScript),
#[serde(alias = "rust")]
Rust(CuddleRustScript),
} }
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct CuddlePlan { pub struct CuddlePlan {
pub base: CuddleBase, pub base: CuddleBase,
pub vars: Option<HashMap<String, String>>, pub vars: Option<CuddlePlanVariables>,
pub scripts: Option<HashMap<String, CuddleScript>>, pub scripts: Option<HashMap<String, CuddleScript>>,
} }
#[derive(Debug, Clone)] #[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct CuddlePlanVariables(HashMap<String, CuddlePlanVar>);
impl From<CuddlePlanVariables> for Vec<CuddleVariable> {
fn from(value: CuddlePlanVariables) -> Self {
let variables: CuddleVariables = value.0.into();
let mut vars = variables.0;
vars.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
vars
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum CuddlePlanVar {
Str(String),
Nested(HashMap<String, CuddlePlanVar>),
}
#[derive(Clone, Debug, PartialEq)]
struct CuddleVariables(Vec<CuddleVariable>);
impl From<HashMap<String, CuddlePlanVar>> for CuddleVariables {
fn from(value: HashMap<String, CuddlePlanVar>) -> Self {
let mut variables = Vec::new();
for (k, v) in value {
match v {
CuddlePlanVar::Str(value) => variables.push(CuddleVariable::new(k, value)),
CuddlePlanVar::Nested(nested) => {
let nested_variables: CuddleVariables = nested.into();
let mut combined_variables: Vec<_> = nested_variables
.0
.into_iter()
.map(|v| CuddleVariable::new(format!("{}_{}", k, v.name), v.value))
.collect();
variables.append(&mut combined_variables);
}
}
}
CuddleVariables(variables)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, serde::Serialize)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct CuddleVariable { pub struct CuddleVariable {
pub name: String, pub name: String,
@ -73,7 +135,102 @@ pub struct CuddleVariable {
} }
impl CuddleVariable { impl CuddleVariable {
pub fn new(name: String, value: String) -> Self { pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self { name, value } Self {
name: name.into(),
value: value.into(),
}
}
}
impl From<HashMap<String, CuddlePlanVar>> for CuddlePlanVar {
fn from(value: HashMap<String, CuddlePlanVar>) -> Self {
CuddlePlanVar::Nested(value)
}
}
impl From<HashMap<&str, CuddlePlanVar>> for CuddlePlanVar {
fn from(value: HashMap<&str, CuddlePlanVar>) -> Self {
CuddlePlanVar::Nested(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
}
impl From<String> for CuddlePlanVar {
fn from(value: String) -> Self {
CuddlePlanVar::Str(value)
}
}
impl From<&str> for CuddlePlanVar {
fn from(value: &str) -> Self {
CuddlePlanVar::Str(value.to_string())
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use super::{CuddlePlanVariables, CuddleVariable};
#[test]
pub fn parse_cuddle_variables() {
let cuddle = r#"
someKey: someValue
someNestedKey:
someNestedNestedKey:
someKey: key
someKey: key
"#;
let cuddle_var: CuddlePlanVariables = serde_yaml::from_str(cuddle).unwrap();
let mut expected = HashMap::new();
expected.insert("someKey", "someValue".into());
let mut nested_key = HashMap::new();
nested_key.insert("someKey", "key".into());
let mut nested_nested_key = HashMap::new();
nested_nested_key.insert("someKey", "key".into());
nested_key.insert("someNestedNestedKey", nested_nested_key.into());
expected.insert("someNestedKey", nested_key.into());
assert_eq!(
CuddlePlanVariables(
expected
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
),
cuddle_var
);
}
#[test]
pub fn to_cuddle_variables() {
let cuddle = r#"
someKey: someValue
someNestedKey:
someNestedNestedKey:
someKey: key
someKey: key
"#;
let cuddle_var: CuddlePlanVariables = serde_yaml::from_str(cuddle).unwrap();
let variables: Vec<CuddleVariable> = cuddle_var.into();
let mut expected: Vec<CuddleVariable> = vec![
CuddleVariable::new("someKey", "someValue"),
CuddleVariable::new("someNestedKey_someKey", "key"),
CuddleVariable::new("someNestedKey_someNestedNestedKey_someKey", "key"),
];
expected.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
assert_eq!(expected, variables);
} }
} }

View File

@ -20,7 +20,16 @@ RUN cargo install --target x86_64-unknown-linux-musl --path cuddle
FROM docker:dind FROM docker:dind
RUN apk add bash git RUN apk add bash git kubectl
ENV SLICE_VERSION=v1.2.7
RUN wget -O kubectl-slice_linux_x86_64.tar.gz \
"https://github.com/patrickdappollonio/kubectl-slice/releases/download/$SLICE_VERSION/kubectl-slice_linux_x86_64.tar.gz" && \
tar -xf kubectl-slice_linux_x86_64.tar.gz && \
chmod +x ./kubectl-slice && \
mv ./kubectl-slice /usr/local/bin/kubectl-slice && \
rm kubectl-slice_linux_x86_64.tar.gz
RUN eval `ssh-agent` RUN eval `ssh-agent`
COPY --from=1password/op:2 /usr/local/bin/op /usr/local/bin/op COPY --from=1password/op:2 /usr/local/bin/op /usr/local/bin/op