feat: with init command

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2023-06-17 03:38:48 +02:00
parent 91ee9d4387
commit 6c5fed87b1
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
13 changed files with 1914 additions and 310 deletions

1898
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,3 @@
[workspace] [workspace]
members = ["cuddle_cli", "examples/base"] members = ["cuddle_cli", "examples/base", "ci"]
resolver = "2"

11
ci/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "ci"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk = "0.2.22"
eyre = "0.6.8"
tokio = { version = "1.28.2", features = ["full"] }

112
ci/src/main.rs Normal file
View File

@ -0,0 +1,112 @@
use std::sync::Arc;
use dagger_sdk::{
Container, ContainerPublishOptsBuilder, Directory, HostDirectoryOptsBuilder, Query,
QueryContainerOptsBuilder,
};
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let src = client.host().directory_opts(
".",
HostDirectoryOptsBuilder::default()
.include(vec![
"ci/",
"cuddle_cli/",
"examples",
"Cargo.lock",
"Cargo.toml",
])
.build()?,
);
client
.container()
.publish_opts(
"kasperhermansen/cuddle:dev",
ContainerPublishOptsBuilder::default()
.platform_variants(vec![
dind_image(client.clone(), src.clone(), "x86_64", "linux/amd64")
.await?
.id()
.await?,
dind_image(client.clone(), src.clone(), "aarch64", "linux/arm64/v8")
.await?
.id()
.await?,
])
.build()?,
)
.await?;
Ok(())
}
async fn dind_image(
client: Arc<Query>,
src: Directory,
architecture: &str,
platform: &str,
) -> eyre::Result<Container> {
let rust_bin = client
.container_opts(
QueryContainerOptsBuilder::default()
.platform(platform)
.build()?,
)
.from("rust:1.70.0-slim-bullseye")
.with_exec(vec![
"rustup",
"target",
"add",
&format!("{architecture}-unknown-linux-musl"),
])
.with_exec(vec!["update-ca-certificates"])
.with_exec(vec!["apt-get", "update"])
.with_exec(vec!["apt-get", "upgrade", "-y"])
.with_exec(vec![
"apt-get",
"install",
"-y",
"-q",
"build-essential",
"curl",
"git",
"musl-tools",
"musl-dev",
"libz-dev",
])
.with_workdir("/app/cuddle/")
.with_directory(".", src.id().await?)
.with_exec(vec![
"cargo",
"install",
"--target",
&format!("{architecture}-unknown-linux-musl"),
"--path",
"cuddle_cli",
"--profile=release",
]);
let final_image = client
.container_opts(
QueryContainerOptsBuilder::default()
.platform(platform)
.build()?,
)
.from("docker:dind")
.with_directory(
"/usr/local/cargo/bin/",
rust_bin.directory("/usr/local/cargo/bin/").id().await?,
);
let path_env = final_image.env_variable("PATH").await?;
let final_image = final_image
.with_env_variable("PATH", format!("{path_env}:/usr/local/cargo/bin"))
.with_exec(vec![""]);
Ok(final_image)
}

View File

@ -15,12 +15,22 @@ anyhow = "1.0.60"
serde = { version = "1.0.143", features = ["derive"] } serde = { version = "1.0.143", features = ["derive"] }
serde_yaml = "0.9.4" serde_yaml = "0.9.4"
walkdir = "2.3.2" walkdir = "2.3.2"
git2 = { version = "0.15.0", features = ["ssh"] } git2 = { version = "0.17.2", default-features = false, features = [
clap = "3.2.16" "vendored-libgit2",
"vendored-openssl",
] }
clap = { version = "4.3.4", features = ["env", "string"] }
envconfig = "0.10.0" envconfig = "0.10.0"
dirs = "4.0.0" dirs = "5.0.1"
tracing = "0.1.36" tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["json"] } tracing-subscriber = { version = "0.3.15", features = ["json"] }
log = { version = "0.4.17", features = ["std", "kv_unstable"] } log = { version = "0.4.17", features = ["std", "kv_unstable"] }
openssl = { version = "0.10", features = ["vendored"] }
tera = "1.17.0" tera = "1.17.0"
openssl = { version = "0.10.54", features = ["vendored"] }
libz-sys = { version = "1.1.9", default-features = false, features = [
"libc",
"static",
] }
inquire = { version = "0.6.2", features = ["console"] }
tempfile = { version = "3.6.0" }
serde_json = "1.0.97"

View File

@ -18,20 +18,20 @@ use crate::{
use self::subcommands::render_template::RenderTemplateCommand; use self::subcommands::render_template::RenderTemplateCommand;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CuddleCli<'a> { pub struct CuddleCli {
scripts: Vec<CuddleAction>, scripts: Vec<CuddleAction>,
variables: Vec<CuddleVariable>, variables: Vec<CuddleVariable>,
context: Arc<Mutex<Vec<CuddleContext>>>, context: Arc<Mutex<Vec<CuddleContext>>>,
command: Option<Command<'a>>, command: Option<Command>,
tmp_dir: Option<PathBuf>, tmp_dir: Option<PathBuf>,
config: CuddleConfig, config: CuddleConfig,
} }
impl<'a> CuddleCli<'a> { impl CuddleCli {
pub fn new( pub fn new(
context: Arc<Mutex<Vec<CuddleContext>>>, context: Arc<Mutex<Vec<CuddleContext>>>,
config: CuddleConfig, config: CuddleConfig,
) -> anyhow::Result<CuddleCli<'a>> { ) -> anyhow::Result<CuddleCli> {
let mut cli = CuddleCli { let mut cli = CuddleCli {
scripts: vec![], scripts: vec![],
variables: vec![], variables: vec![],
@ -159,6 +159,7 @@ impl<'a> CuddleCli<'a> {
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::init::build_command(root_cmd, self.clone());
self.command = Some(root_cmd); self.command = Some(root_cmd);
@ -176,13 +177,15 @@ impl<'a> CuddleCli<'a> {
.and_then(|cmd| cmd.execute())?; .and_then(|cmd| cmd.execute())?;
Ok(()) Ok(())
} }
Some(("init", sub_matches)) => {
subcommands::init::execute_init(sub_matches, self.clone())
}
_ => Err(anyhow::anyhow!("could not find a match")), _ => Err(anyhow::anyhow!("could not find a match")),
}; };
match res { match res {
Ok(()) => {} Ok(()) => {}
Err(e) => { Err(e) => {
let _ = cli.print_long_help();
return Err(e); return Err(e);
} }
} }

View File

@ -0,0 +1,147 @@
use std::fs::{create_dir_all, read, read_dir};
use std::io::Write;
use clap::{ArgMatches, Command};
use crate::cli::CuddleCli;
pub fn build_command(root_cmd: Command, cli: CuddleCli) -> Command {
let mut repo_url = clap::Arg::new("repo").long("repo").short('r');
if let Ok(cuddle_template_url) = std::env::var("CUDDLE_TEMPLATE_URL") {
repo_url = repo_url.default_value(cuddle_template_url);
} else {
repo_url = repo_url.required(true);
}
let mut execute_cmd = Command::new("init")
.about("init bootstraps a repository from a template")
.arg(repo_url)
.arg(clap::Arg::new("name"))
.arg(clap::Arg::new("path"));
root_cmd.subcommand(execute_cmd)
}
pub fn execute_init(exe_submatch: &ArgMatches, cli: CuddleCli) -> anyhow::Result<()> {
let repo = exe_submatch.get_one::<String>("repo").unwrap();
let name = exe_submatch.get_one::<String>("name");
let path = exe_submatch.get_one::<String>("path");
tracing::info!("Downloading: {}", repo);
create_dir_all(std::env::temp_dir())?;
let tmpdir = tempfile::tempdir()?;
let tmpdir_path = tmpdir.path().canonicalize()?;
let output = std::process::Command::new("git")
.args(&["clone", repo, "."])
.current_dir(tmpdir_path)
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
let templates_path = tmpdir.path().join("cuddle-templates.json");
let template_path = tmpdir.path().join("cuddle-template.json");
let templates = if templates_path.exists() {
let templates = read(templates_path)?;
let templates: CuddleTemplates = serde_json::from_slice(&templates)?;
let mut single_templates = Vec::new();
for template_name in templates.templates.iter() {
let template = read(
tmpdir
.path()
.join(template_name)
.join("cuddle-template.json"),
)?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
single_templates.push((template_name, template))
}
single_templates
.into_iter()
.map(|(name, template)| (name.clone(), tmpdir.path().join(name), template))
.collect::<Vec<_>>()
} else if template_path.exists() {
let template = read(template_path)?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
vec![(template.clone().name, tmpdir.path().to_path_buf(), template)]
} else {
anyhow::bail!("No cuddle-template.json or cuddle-templates.json found");
};
let template = match name {
Some(name) => {
let template = read(tmpdir.path().join(name).join("cuddle-template.json"))?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
Ok((name.clone(), tmpdir.path().join(name), template))
}
None => {
if templates.len() > 1 {
let name = inquire::Select::new(
"template",
templates.iter().map(|t| t.0.clone()).collect(),
)
.with_help_message("name of which template to use")
.prompt()?;
let found_template = templates
.iter()
.find(|item| item.0 == name)
.ok_or(anyhow::anyhow!("could not find an item with that name"))?;
Ok(found_template.clone())
} else if templates.len() == 1 {
Ok(templates[0].clone())
} else {
Err(anyhow::anyhow!("No templates found, with any valid names"))
}
}
};
let (name, template_dir, template) = template?;
let path = match path {
Some(path) => path.clone(),
None => inquire::Text::new("path")
.with_help_message("to where it should be placed")
.with_default(".")
.prompt()?,
};
create_dir_all(&path)?;
let dir = std::fs::read_dir(&path)?;
if dir.count() != 0 {
anyhow::bail!("Directory {} is not empty", &path);
}
for entry in read_dir(template_dir)? {
let entry = entry?;
let entry_path = entry.path();
let name = entry.file_name();
if name == "cuddle-template.json" || name == "cuddle-templates.json" {
continue;
}
std::fs::rename(entry_path, std::path::PathBuf::from(&path).join(name))?;
}
Ok(())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct CuddleTemplates {
pub templates: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct CuddleTemplate {
pub name: String,
}

View File

@ -1,2 +1,3 @@
pub mod init;
pub mod render_template; pub mod render_template;
pub mod x; pub mod x;

View File

@ -4,7 +4,7 @@ use clap::{Arg, ArgMatches, Command};
use crate::{cli::CuddleCli, model::CuddleVariable}; use crate::{cli::CuddleCli, model::CuddleVariable};
pub fn build_command<'a>(root_cmd: Command<'a>) -> Command<'a> { pub fn build_command(root_cmd: Command) -> Command {
root_cmd.subcommand( root_cmd.subcommand(
Command::new("render_template") Command::new("render_template")
.about("renders a jinja compatible template") .about("renders a jinja compatible template")

View File

@ -2,14 +2,15 @@ use clap::{ArgMatches, Command};
use crate::cli::CuddleCli; use crate::cli::CuddleCli;
pub fn build_command<'a>(root_cmd: Command<'a>, cli: CuddleCli<'a>) -> Command<'a> { pub fn build_command(root_cmd: Command, cli: CuddleCli) -> Command {
if cli.scripts.len() > 0 { if cli.scripts.len() > 0 {
let mut execute_cmd = Command::new("x").about("x is your entry into your domains scripts, scripts inherited from parents will also be present here").subcommand_required(true); let execute_cmd_about = "x is your entry into your domains scripts, scripts inherited from parents will also be present here";
let mut execute_cmd = Command::new("x")
.about(execute_cmd_about)
.subcommand_required(true);
for script in cli.scripts.iter() { for script in cli.scripts.iter() {
let action_cmd = Command::new(script.name.clone()); let action_cmd = Command::new(&script.name).about(&script.name);
// TODO: Some way to add an about for clap, requires conversion from String -> &str
execute_cmd = execute_cmd.subcommand(action_cmd); execute_cmd = execute_cmd.subcommand(action_cmd);
} }

View File

@ -20,10 +20,7 @@ fn main() -> anyhow::Result<()> {
} }
fn init_logging() -> anyhow::Result<()> { fn init_logging() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt().with_max_level(Level::INFO).init();
.pretty()
.with_max_level(Level::INFO)
.init();
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
FROM rust:1.62.1-slim-bullseye as base FROM rust:1.70-slim-bullseye as base
RUN rustup target add x86_64-unknown-linux-musl RUN rustup target add x86_64-unknown-linux-musl

View File

@ -1,2 +1,3 @@
.cuddle/ .cuddle/
.git/ .git/
target/