Compare commits
163 Commits
4765502109
...
feat/try-a
Author | SHA1 | Date | |
---|---|---|---|
d33e42a608
|
|||
62a677ffcd
|
|||
186f13a16c
|
|||
02dd805db4
|
|||
23d68caf71
|
|||
b3aedfb411
|
|||
b01543a8b9
|
|||
5e88ffdbc9
|
|||
61db3da695
|
|||
1ba6cf79c0
|
|||
c2b7e44ea3
|
|||
531ea225f3
|
|||
6cb65e55c1
|
|||
071cb43510
|
|||
78c86dc39a
|
|||
5bc831da10
|
|||
a4213ea61f
|
|||
bf7a7db868
|
|||
1d1ac49d0b
|
|||
8520bcb5b0 | |||
7b136b1331
|
|||
d0d591dd4f | |||
e09e28e8d0
|
|||
3a09c68378
|
|||
8f889663b2 | |||
6d3bdda04d | |||
1720f29149 | |||
dc2a4977d6 | |||
52c7f77751
|
|||
ede55b975b
|
|||
85cc1d46db
|
|||
cc7aaf14eb | |||
1bacfdfb29 | |||
9820d2c3ab | |||
5fae1fc403 | |||
b9f7ff0a6f | |||
440245c332 | |||
a89af69c15 | |||
b93f96053e | |||
347d6171a5 | |||
e7d90ffcc5 | |||
9c607e8ab1 | |||
cf6a4b9fcd | |||
a05699d24e | |||
ae9fdf7e7e | |||
8c3e546b27 | |||
248f294000 | |||
65f4271957 | |||
eb980308c6 | |||
51e5e1a4ce | |||
00e45053f7 | |||
dace148e52 | |||
04774a69bf | |||
c489660211 | |||
2e6e6fd328 | |||
84df1fe4df | |||
30c58b9eb2 | |||
c2502dbf00 | |||
bec95c9519 | |||
02b1a627f0 | |||
4b84f27d67 | |||
a3bb366d07 | |||
c479cadf4d | |||
74bd83aaec | |||
a707d31277 | |||
8cfdebdaee | |||
f721e45f8f | |||
db4b41c032 | |||
4945ecca40 | |||
a6560a10cb | |||
e2feef1c27
|
|||
1b9c9188d4 | |||
a9ca8cdc18 | |||
0431dd03bf | |||
e5eabf6901 | |||
a336025a0c | |||
e471452ed3 | |||
05ea0d8ed8 | |||
c42ad69eef | |||
614e2bf442 | |||
23d9ca8d11 | |||
05d8209f15 | |||
8de0a15922 | |||
e20fbc56b4 | |||
aa1f6c3a16 | |||
e944203e80 | |||
3d4e6b6fcd | |||
35d54ab30f | |||
941a8f600f | |||
32d5fb6f97 | |||
f97fe5c832 | |||
8580813bbc | |||
65a995c9ee | |||
31c77c7581 | |||
dff0e85f7d | |||
ff74c55829 | |||
e6cd1c16b6 | |||
6b7f915c12 | |||
c8e4f4b66d | |||
0aa7195415 | |||
1da2ded83e | |||
60b234c0d7 | |||
cace45ee5b | |||
bc2999fb92
|
|||
26ef1cb0cd
|
|||
2cd9509fcb
|
|||
53b7513ceb | |||
71965cb07c | |||
dd94ee2e8e | |||
3bf7e837e5 | |||
39505938a5 | |||
724a364984 | |||
840694967d | |||
b32643ff7a | |||
41d337d003 | |||
fd01de7ede
|
|||
44e8fe8918
|
|||
b7747dec06
|
|||
5e9dc88a1b
|
|||
ec1fdf267f
|
|||
c11baae1ba | |||
f396a36b37 | |||
8f155373f3 | |||
27b4e73a48 | |||
b58597e25d | |||
ae74427ca8
|
|||
4bd4648a94
|
|||
26c1c083de
|
|||
3a9f00b1e5
|
|||
02ec96e93c
|
|||
3fae1b2b06
|
|||
c2cfee11b2
|
|||
c7fdd4fe17
|
|||
7e008bea09
|
|||
83a8b5729e
|
|||
a17c5d1421
|
|||
68b46e4bec
|
|||
33e1ff0e37
|
|||
32f7ce2459
|
|||
e6d5f8c4b8
|
|||
506a8e4a4e
|
|||
a483e28a70
|
|||
12bd424f18
|
|||
6a82e0c10a
|
|||
f8035357b7
|
|||
dc0fa589a5
|
|||
04e8baeefc
|
|||
2241941f0e
|
|||
2f2fdb9631
|
|||
6c5fed87b1
|
|||
91ee9d4387
|
|||
77bbf6c855 | |||
5b0eecd83d | |||
04f3bc1295
|
|||
1727836c1f
|
|||
aae8201252
|
|||
4e5f451bf6
|
|||
581404c622
|
|||
0ec196e8c9
|
|||
4313c60056
|
|||
21cda03b6d
|
|||
f8c5ae93b1
|
|||
21b779a03d
|
64
.drone.yml
64
.drone.yml
@@ -1,62 +1,2 @@
|
|||||||
kind: pipeline
|
kind: template
|
||||||
name: default
|
load: cuddle-rust-cli-plan.yaml
|
||||||
type: docker
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: load_secret
|
|
||||||
image: debian:buster-slim
|
|
||||||
volumes:
|
|
||||||
- name: ssh
|
|
||||||
path: /root/.ssh/
|
|
||||||
environment:
|
|
||||||
SSH_KEY:
|
|
||||||
from_secret: gitea_id_ed25519
|
|
||||||
commands:
|
|
||||||
- mkdir -p $HOME/.ssh/
|
|
||||||
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519
|
|
||||||
- ls $HOME/.ssh/
|
|
||||||
- cat $HOME/.ssh/id_ed25519
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
image: kasperhermansen/cuddle-image:latest
|
|
||||||
pull: always
|
|
||||||
volumes:
|
|
||||||
- name: ssh
|
|
||||||
path: /root/.ssh/
|
|
||||||
- name: dockersock
|
|
||||||
path: /var/run
|
|
||||||
commands:
|
|
||||||
- apk add bash
|
|
||||||
- cuddle_cli x build_cuddle_image
|
|
||||||
environment:
|
|
||||||
DOCKER_BUILDKIT: 1
|
|
||||||
DOCKER_USERNAME:
|
|
||||||
from_secret: docker_username
|
|
||||||
DOCKER_PASSWORD:
|
|
||||||
from_secret: docker_password
|
|
||||||
depends_on:
|
|
||||||
- "load_secret"
|
|
||||||
|
|
||||||
- name: send telegram notification
|
|
||||||
image: appleboy/drone-telegram
|
|
||||||
settings:
|
|
||||||
token:
|
|
||||||
from_secret: telegram_token
|
|
||||||
to: 2129601481
|
|
||||||
format: markdown
|
|
||||||
when:
|
|
||||||
status: [failure]
|
|
||||||
|
|
||||||
services:
|
|
||||||
- name: docker
|
|
||||||
image: docker:dind
|
|
||||||
privileged: true
|
|
||||||
volumes:
|
|
||||||
- name: dockersock
|
|
||||||
path: /var/run
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: ssh
|
|
||||||
temp: {}
|
|
||||||
- name: dockersock
|
|
||||||
temp: {}
|
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
/target
|
target/
|
||||||
.cuddle/
|
.cuddle/
|
||||||
|
1402
Cargo.lock
generated
1402
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,5 +1,19 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = ["crates/*"]
|
||||||
"cuddle_cli",
|
resolver = "2"
|
||||||
"examples/base"
|
|
||||||
]
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
cuddle = { path = "crates/cuddle" }
|
||||||
|
|
||||||
|
anyhow = { version = "1" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
|
tracing-subscriber = { version = "0.3.18" }
|
||||||
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
|
dotenv = { version = "0.15" }
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
|
154
README.md
Normal file
154
README.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Cuddle
|
||||||
|
|
||||||
|
Cuddle aims to reduce the complexity of building code projects. It allows either
|
||||||
|
individuals or organisations to share scripts and workflows, as well as keep a
|
||||||
|
dynamic inventory of their code.
|
||||||
|
|
||||||
|
At its most basic allows enforcing a schema for projects, further it allows the
|
||||||
|
sharing of scripts, pipelines, templates, and much more.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuddle init
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuddle is meant to be used in the degree that it makes sense for you, it can be
|
||||||
|
adopted quickly to improve code sharing, and be fully bought into to provide a
|
||||||
|
full suite of project level solutions to offload requirements of developers.
|
||||||
|
Start small with scripts and plans, and gradually adopt features from there as
|
||||||
|
you need them.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Cuddle is primarily a cli based tool, but ships with optional server components.
|
||||||
|
Server components helps enhance the features of cuddle, such that variables can
|
||||||
|
be enforced at runtime, actions be downloaded instead of built and much more. It
|
||||||
|
all works on a gradual adoption process, so as an operator you can gradually add
|
||||||
|
features as you mature and need them.
|
||||||
|
|
||||||
|
A cuddle workflow is split up into:
|
||||||
|
|
||||||
|
- Projects: A project is what most users interface with, you can think of it as
|
||||||
|
the cockpit of a car. You can use all the bells and whistles, but somebody
|
||||||
|
else has built up all the user journeys you're interacting with.
|
||||||
|
- Plans: A plan is the engine room of the car, it ties together components,
|
||||||
|
features and requirements for use in the cockpit. The plan usually faciliates
|
||||||
|
most of the infrastructure an application stands to outsource. Such as scripts
|
||||||
|
to run, build, test an application, build its templates for deployment, run
|
||||||
|
pipelines, common actions. Plans specialize in building preciely what the
|
||||||
|
projects needs, as such your organisation or yourself, should only have a
|
||||||
|
handful of them at most. To further reduce duplication of work between plans,
|
||||||
|
components can be used to abstract common features required by plans, such as
|
||||||
|
templating, individual components for templates.
|
||||||
|
- Components: Components are a slice of features not useful in of itself, but
|
||||||
|
used by plans to further their behavior, a sample component may be a template
|
||||||
|
part, which includes a list of allowed ip addresses for internal
|
||||||
|
communication, it may be how to build an ingress, ship a docker container to a
|
||||||
|
registry, basically small individual components useful for a larger whole.
|
||||||
|
- Actions: are code units that can take a variety of forms, golang, rust, bash,
|
||||||
|
what have you. All of them are accessed by `cuddle do`, individual components
|
||||||
|
can take the form of actions, if desired
|
||||||
|
- Global: Is a set of actions and features that are available anywhere a user
|
||||||
|
might need them. For example it can be a solution to easily log into
|
||||||
|
production environments, release code to production, get the menu from the
|
||||||
|
canteen, etc.
|
||||||
|
- Personal: Is a config an org can decide the users and develoeprs fill out, it
|
||||||
|
can help other tooling better enhance the experience. For example it may be
|
||||||
|
useful to always have the developers email available, if for example we want
|
||||||
|
to trigger an automatic login for them.
|
||||||
|
|
||||||
|
### Init
|
||||||
|
|
||||||
|
`cuddle init` will bootstrap a project either from scratch, or just adding
|
||||||
|
required `cuddle.toml` parts.
|
||||||
|
|
||||||
|
A `cuddle.toml` is required to access project level `cuddle` commands. Such as
|
||||||
|
`cuddle do`, `cuddle get`, `cuddle validate`, etc.
|
||||||
|
|
||||||
|
`cuddle.toml` looks something like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "some-cuddle-project"
|
||||||
|
owner = "kjuulh"
|
||||||
|
```
|
||||||
|
|
||||||
|
What is generated out of the box is a _bare_ project. A bare project doesn't
|
||||||
|
share any features, or enforce any requirements on its schema from its plan. It
|
||||||
|
is most useful for projects that doesn't fit any mold, or for individual users
|
||||||
|
simply testing out the framework.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
`cuddle actions` are project level scripts that can take a variety of forms.
|
||||||
|
Actions are invoked via. `cuddle do` when inside of a project, most projects
|
||||||
|
won't build actions themselves, instead they will depend on what their plan
|
||||||
|
provides for them.
|
||||||
|
|
||||||
|
Actions can be bootstrapped via. `cuddle init actions`, an action is slice of a
|
||||||
|
cli. Cuddle provides a convenient way of building them, such that they are easy
|
||||||
|
to build, maintain and operate. Most actions are written in either golang or
|
||||||
|
rust, but bash and lua is also available.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
# ...
|
||||||
|
|
||||||
|
[project.actions.run]
|
||||||
|
type = "bash"
|
||||||
|
command = """
|
||||||
|
cargo run -p some-cuddle-project -- $@
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
This action can be invoked via.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuddle do run
|
||||||
|
```
|
||||||
|
|
||||||
|
Scripts are also based on convention, so if a rust action is used:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuddle init action rust
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing will be added to the `cuddle.toml` instead you'll receive a
|
||||||
|
`actions/rust/` project where you can fill out the clis according to the
|
||||||
|
template given.
|
||||||
|
|
||||||
|
### Plans
|
||||||
|
|
||||||
|
Plans are a crucial component for code sharing, enforcement of values, metrics
|
||||||
|
and so on. Plans provide a predefined journey for how to work with a specific
|
||||||
|
type of application. I.e. what does our organisation think a Rust application
|
||||||
|
look like?
|
||||||
|
|
||||||
|
Plans are maintained via. the `plan` section of the `cuddle.toml` file
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plan]
|
||||||
|
git = "https://github.com/kjuulh/some-cuddle-plan.git"
|
||||||
|
branch = "main"
|
||||||
|
|
||||||
|
# Alternatively
|
||||||
|
plan = "https://github.com/kjuulh/some-cuddle-plan.git" # if you want the default
|
||||||
|
|
||||||
|
[project]
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
A plan itself will be maintained via. a `cuddle.plan.toml` file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuddle init plan
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plan]
|
||||||
|
name = "some-cuddle-plan"
|
||||||
|
|
||||||
|
[plan.components]
|
||||||
|
canteen = {git = "https://github.com/kjuulh/canteen"}
|
||||||
|
ingress = {git = "https://github.com/kjuulh/cuddle-ingress"}
|
||||||
|
ip-allowlist = {git = "https://github.com/kjuulh/ip-allowlist"}
|
||||||
|
```
|
13
crates/cuddle-actions/Cargo.toml
Normal file
13
crates/cuddle-actions/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "cuddle-actions"
|
||||||
|
edition = "2021"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
clap = { workspace = true, features = ["string"] }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1.4.0"
|
120
crates/cuddle-actions/src/lib.rs
Normal file
120
crates/cuddle-actions/src/lib.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// Cuddle actions is a two part action, it is called from cuddle itself, second the cli uses a provided sdk to expose functionality
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, ffi::OsString, io::Write};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
// Fix design make it so that it works like axum!
|
||||||
|
|
||||||
|
type ActionFn = dyn Fn() -> anyhow::Result<()> + 'static;
|
||||||
|
|
||||||
|
struct Action {
|
||||||
|
name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
f: Box<ActionFn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
struct ActionSchema<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
description: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CuddleActions {
|
||||||
|
actions: BTreeMap<String, Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct AddActionOptions {
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CuddleActions {
|
||||||
|
pub fn add_action<F>(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
action_fn: F,
|
||||||
|
options: &AddActionOptions,
|
||||||
|
) -> &mut Self
|
||||||
|
where
|
||||||
|
F: Fn() -> anyhow::Result<()> + 'static,
|
||||||
|
{
|
||||||
|
self.actions.insert(
|
||||||
|
name.into(),
|
||||||
|
Action {
|
||||||
|
name: name.into(),
|
||||||
|
description: options.description.clone(),
|
||||||
|
f: Box::new(action_fn),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.execute_from(std::env::args())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_from<I, T>(&mut self, items: I) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: Into<OsString> + Clone,
|
||||||
|
{
|
||||||
|
let mut do_cmd = clap::Command::new("do").subcommand_required(true);
|
||||||
|
|
||||||
|
for action in self.actions.values() {
|
||||||
|
let mut do_action_cmd = clap::Command::new(action.name.clone());
|
||||||
|
if let Some(description) = &action.description {
|
||||||
|
do_action_cmd = do_action_cmd.about(description.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
do_cmd = do_cmd.subcommand(do_action_cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = clap::Command::new("cuddle-action")
|
||||||
|
.subcommand_required(true)
|
||||||
|
.subcommand(clap::Command::new("schema"))
|
||||||
|
.subcommand(do_cmd);
|
||||||
|
|
||||||
|
let matches = root.try_get_matches_from(items)?;
|
||||||
|
match matches.subcommand().expect("subcommand to be required") {
|
||||||
|
("schema", _args) => {
|
||||||
|
let output = self.get_pretty_actions()?;
|
||||||
|
|
||||||
|
// Write all stdout to buffer
|
||||||
|
std::io::stdout().write_all(output.as_bytes())?;
|
||||||
|
std::io::stdout().write_all("\n".as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
("do", args) => {
|
||||||
|
let (command_name, _args) = args.subcommand().unwrap();
|
||||||
|
match self.actions.get_mut(command_name) {
|
||||||
|
Some(action) => (*action.f)(),
|
||||||
|
None => {
|
||||||
|
anyhow::bail!("command not found: {}", command_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("no command found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pretty_actions(&self) -> anyhow::Result<String> {
|
||||||
|
let schema = self
|
||||||
|
.actions
|
||||||
|
.values()
|
||||||
|
.map(|a| ActionSchema {
|
||||||
|
name: &a.name,
|
||||||
|
description: a.description.as_deref(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let output = serde_json::to_string_pretty(&schema)?;
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
67
crates/cuddle-actions/tests/mod.rs
Normal file
67
crates/cuddle-actions/tests/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use cuddle_actions::AddActionOptions;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_schema_no_actions() -> anyhow::Result<()> {
|
||||||
|
let output = cuddle_actions::CuddleActions::default().get_pretty_actions()?;
|
||||||
|
|
||||||
|
assert_eq!("[]", &output);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_schema_simple_action() -> anyhow::Result<()> {
|
||||||
|
let output = cuddle_actions::CuddleActions::default()
|
||||||
|
.add_action("something", || Ok(()), &AddActionOptions::default())
|
||||||
|
.get_pretty_actions()?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"name": "something",
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
&output
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_call_simple_action() -> anyhow::Result<()> {
|
||||||
|
cuddle_actions::CuddleActions::default()
|
||||||
|
.add_action("something", || Ok(()), &AddActionOptions::default())
|
||||||
|
.execute_from(vec!["cuddle-actions", "do", "something"])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_fail_on_unknown_command() -> anyhow::Result<()> {
|
||||||
|
let res = cuddle_actions::CuddleActions::default().execute_from(vec![
|
||||||
|
"cuddle-actions",
|
||||||
|
"do",
|
||||||
|
"something",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_cmd_can_fail() -> anyhow::Result<()> {
|
||||||
|
let res = cuddle_actions::CuddleActions::default()
|
||||||
|
.add_action(
|
||||||
|
"something",
|
||||||
|
|| anyhow::bail!("failed to run cmd"),
|
||||||
|
&AddActionOptions::default(),
|
||||||
|
)
|
||||||
|
.execute_from(vec!["cuddle-actions", "do", "something"]);
|
||||||
|
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1
crates/cuddle/.gitignore
vendored
Normal file
1
crates/cuddle/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
19
crates/cuddle/Cargo.toml
Normal file
19
crates/cuddle/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "cuddle"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
dotenv.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
|
||||||
|
toml = "0.8.19"
|
||||||
|
fs_extra = "1.3.0"
|
1
crates/cuddle/examples/basic/plan/cuddle.plan.toml
Normal file
1
crates/cuddle/examples/basic/plan/cuddle.plan.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[plan]
|
0
crates/cuddle/examples/basic/plan/cuddle.toml
Normal file
0
crates/cuddle/examples/basic/plan/cuddle.toml
Normal file
5
crates/cuddle/examples/basic/project/cuddle.toml
Normal file
5
crates/cuddle/examples/basic/project/cuddle.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[plan]
|
||||||
|
path = "../plan"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "basic"
|
5
crates/cuddle/examples/git/cuddle.toml
Normal file
5
crates/cuddle/examples/git/cuddle.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[plan]
|
||||||
|
git = "ssh://git@git.front.kjuulh.io/kjuulh/cuddle-git-example.git"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "git"
|
@@ -0,0 +1 @@
|
|||||||
|
{"ProjectSchema":{"fields":{"name":{"fields":null,"type":null,"contracts":["String"],"documentation":null}},"type":null,"contracts":[],"documentation":null}}
|
2
crates/cuddle/examples/schema/plan/cuddle.plan.toml
Normal file
2
crates/cuddle/examples/schema/plan/cuddle.plan.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[plan]
|
||||||
|
schema = { nickel = "schema.ncl" }
|
0
crates/cuddle/examples/schema/plan/cuddle.toml
Normal file
0
crates/cuddle/examples/schema/plan/cuddle.toml
Normal file
5
crates/cuddle/examples/schema/plan/schema.ncl
Normal file
5
crates/cuddle/examples/schema/plan/schema.ncl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
ProjectSchema = {
|
||||||
|
name | String,..
|
||||||
|
}
|
||||||
|
}
|
5
crates/cuddle/examples/schema/project/cuddle.toml
Normal file
5
crates/cuddle/examples/schema/project/cuddle.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[project]
|
||||||
|
name = "schema"
|
||||||
|
|
||||||
|
[plan]
|
||||||
|
path = "../plan"
|
88
crates/cuddle/src/cli.rs
Normal file
88
crates/cuddle/src/cli.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use std::{borrow::BorrowMut, io::Write};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use get_command::GetCommand;
|
||||||
|
|
||||||
|
use crate::{cuddle_state::Cuddle, state::ValidatedState};
|
||||||
|
|
||||||
|
mod get_command;
|
||||||
|
|
||||||
|
pub struct Cli {
|
||||||
|
cli: clap::Command,
|
||||||
|
cuddle: Cuddle<ValidatedState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub fn new(cuddle: Cuddle<ValidatedState>) -> Self {
|
||||||
|
let cli = clap::Command::new("cuddle").subcommand_required(true);
|
||||||
|
|
||||||
|
Self { cli, cuddle }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup(mut self) -> anyhow::Result<Self> {
|
||||||
|
let commands = self.get_commands().await?;
|
||||||
|
|
||||||
|
self.cli = self.cli.subcommands(commands);
|
||||||
|
|
||||||
|
// TODO: Add global
|
||||||
|
// TODO: Add components
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_commands(&self) -> anyhow::Result<Vec<clap::Command>> {
|
||||||
|
Ok(vec![
|
||||||
|
clap::Command::new("do").subcommand_required(true),
|
||||||
|
clap::Command::new("get")
|
||||||
|
.about(GetCommand::description())
|
||||||
|
.arg(
|
||||||
|
clap::Arg::new("query")
|
||||||
|
.required(true)
|
||||||
|
.help("query is how values are extracted, '.project.name' etc"),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(self) -> anyhow::Result<()> {
|
||||||
|
match self
|
||||||
|
.cli
|
||||||
|
.get_matches_from(std::env::args())
|
||||||
|
.subcommand()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to find subcommand"))?
|
||||||
|
{
|
||||||
|
("do", _args) => {
|
||||||
|
tracing::debug!("executing do");
|
||||||
|
}
|
||||||
|
("get", args) => {
|
||||||
|
let query = args
|
||||||
|
.get_one::<String>("query")
|
||||||
|
.ok_or(anyhow!("query is required"))?;
|
||||||
|
|
||||||
|
let res = GetCommand::new(self.cuddle).execute(query).await?;
|
||||||
|
|
||||||
|
std::io::stdout().write_all(res.as_bytes())?;
|
||||||
|
std::io::stdout().write_all("\n".as_bytes())?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_project_commands(&self) -> anyhow::Result<Vec<clap::Command>> {
|
||||||
|
if let Some(_project) = self.cuddle.state.project.as_ref() {
|
||||||
|
// Add project level commands
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_plan_commands(self) -> anyhow::Result<Self> {
|
||||||
|
if let Some(_plan) = self.cuddle.state.plan.as_ref() {
|
||||||
|
// Add plan level commands
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
124
crates/cuddle/src/cli/get_command.rs
Normal file
124
crates/cuddle/src/cli/get_command.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use crate::{
|
||||||
|
cuddle_state::Cuddle,
|
||||||
|
state::{
|
||||||
|
validated_project::{Project, Value},
|
||||||
|
ValidatedState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct GetCommand {
|
||||||
|
query_engine: ProjectQueryEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetCommand {
|
||||||
|
pub fn new(cuddle: Cuddle<ValidatedState>) -> Self {
|
||||||
|
Self {
|
||||||
|
query_engine: ProjectQueryEngine::new(
|
||||||
|
&cuddle
|
||||||
|
.state
|
||||||
|
.project
|
||||||
|
.expect("we should always have a project if get command is available"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(&self, query: &str) -> anyhow::Result<String> {
|
||||||
|
let res = self
|
||||||
|
.query_engine
|
||||||
|
.query(query)?
|
||||||
|
.ok_or(anyhow::anyhow!("query was not found in project"))?;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Value::String(s) => Ok(s),
|
||||||
|
Value::Bool(b) => Ok(b.to_string()),
|
||||||
|
Value::Array(value) => {
|
||||||
|
let val = serde_json::to_string_pretty(&value)?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
Value::Map(value) => {
|
||||||
|
let val = serde_json::to_string_pretty(&value)?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description() -> String {
|
||||||
|
"get returns a given variable from the project given a key, following a jq like schema (.project.name, etc.)"
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectQueryEngine {
|
||||||
|
project: Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectQueryEngine {
|
||||||
|
pub fn new(project: &Project) -> Self {
|
||||||
|
Self {
|
||||||
|
project: project.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query(&self, query: &str) -> anyhow::Result<Option<Value>> {
|
||||||
|
let parts = query
|
||||||
|
.split('.')
|
||||||
|
.filter(|i| !i.is_empty())
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
Ok(self.traverse(&parts, &self.project.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn traverse(&self, query: &[&str], value: &Value) -> Option<Value> {
|
||||||
|
match query.split_first() {
|
||||||
|
Some((key, rest)) => match value {
|
||||||
|
Value::Map(items) => {
|
||||||
|
let item = items.get(*key)?;
|
||||||
|
|
||||||
|
self.traverse(rest, item)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::warn!(
|
||||||
|
"key: {} doesn't have a corresponding value: {:?}",
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Some(value.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_can_query_item() -> anyhow::Result<()> {
|
||||||
|
let project = ProjectQueryEngine::new(&Project {
|
||||||
|
value: Value::Map(
|
||||||
|
[(
|
||||||
|
String::from("project"),
|
||||||
|
Value::Map(
|
||||||
|
[(
|
||||||
|
String::from("name"),
|
||||||
|
Value::String(String::from("something")),
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
root: PathBuf::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = project.query(".project.name")?;
|
||||||
|
|
||||||
|
assert_eq!(Some(Value::String("something".into())), res);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
68
crates/cuddle/src/cuddle_state.rs
Normal file
68
crates/cuddle/src/cuddle_state.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use crate::plan::{ClonedPlan, Plan};
|
||||||
|
use crate::project::ProjectPlan;
|
||||||
|
use crate::state::{self, ValidatedState};
|
||||||
|
|
||||||
|
pub struct Start {}
|
||||||
|
pub struct PrepareProject {
|
||||||
|
project: Option<ProjectPlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PreparePlan {
|
||||||
|
project: Option<ProjectPlan>,
|
||||||
|
plan: Option<ClonedPlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Cuddle<S = Start> {
|
||||||
|
pub state: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cuddle maintains the context for cuddle to use
|
||||||
|
// Stage 1 figure out which state to display
|
||||||
|
// Stage 2 prepare plan
|
||||||
|
// Stage 3 validate settings, build actions, prepare
|
||||||
|
|
||||||
|
impl Cuddle<Start> {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self { state: Start {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_project(&self) -> anyhow::Result<Cuddle<PrepareProject>> {
|
||||||
|
let project = ProjectPlan::from_current_path().await?;
|
||||||
|
|
||||||
|
Ok(Cuddle {
|
||||||
|
state: PrepareProject { project },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cuddle<PrepareProject> {
|
||||||
|
pub async fn prepare_plan(&self) -> anyhow::Result<Cuddle<PreparePlan>> {
|
||||||
|
let plan = if let Some(project) = &self.state.project {
|
||||||
|
Plan::new().clone_from_project(project).await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Cuddle {
|
||||||
|
state: PreparePlan {
|
||||||
|
project: self.state.project.clone(),
|
||||||
|
plan,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cuddle<PreparePlan> {
|
||||||
|
pub async fn build_state(&self) -> anyhow::Result<Cuddle<ValidatedState>> {
|
||||||
|
let state = if let Some(project) = &self.state.project {
|
||||||
|
let state = state::State::new();
|
||||||
|
let raw_state = state.build_state(project, &self.state.plan).await?;
|
||||||
|
|
||||||
|
state.validate_state(&raw_state).await?
|
||||||
|
} else {
|
||||||
|
ValidatedState::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Cuddle { state })
|
||||||
|
}
|
||||||
|
}
|
27
crates/cuddle/src/main.rs
Normal file
27
crates/cuddle/src/main.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use cli::Cli;
|
||||||
|
use cuddle_state::Cuddle;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod cuddle_state;
|
||||||
|
mod plan;
|
||||||
|
mod project;
|
||||||
|
mod schema_validator;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let cuddle = Cuddle::default()
|
||||||
|
.prepare_project()
|
||||||
|
.await?
|
||||||
|
.prepare_plan()
|
||||||
|
.await?
|
||||||
|
.build_state()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Cli::new(cuddle).setup().await?.execute().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
202
crates/cuddle/src/plan.rs
Normal file
202
crates/cuddle/src/plan.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use fs_extra::dir::CopyOptions;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::project::{self, ProjectPlan};
|
||||||
|
|
||||||
|
pub const CUDDLE_PLAN_FOLDER: &str = "plan";
|
||||||
|
pub const CUDDLE_PROJECT_WORKSPACE: &str = ".cuddle";
|
||||||
|
pub const CUDDLE_PLAN_FILE: &str = "cuddle.plan.toml";
|
||||||
|
|
||||||
|
pub trait PlanPathExt {
|
||||||
|
fn plan_path(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlanPathExt for project::ProjectPlan {
|
||||||
|
fn plan_path(&self) -> PathBuf {
|
||||||
|
self.root
|
||||||
|
.join(CUDDLE_PROJECT_WORKSPACE)
|
||||||
|
.join(CUDDLE_PLAN_FOLDER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RawPlan {
|
||||||
|
pub config: RawPlanConfig,
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawPlan {
|
||||||
|
pub fn new(config: RawPlanConfig, root: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
root: root.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
|
||||||
|
let config: RawPlanConfig = toml::from_str(content)?;
|
||||||
|
|
||||||
|
Ok(Self::new(config, root))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let cuddle_file = path.join(CUDDLE_PLAN_FILE);
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
path = cuddle_file.display().to_string(),
|
||||||
|
"searching for cuddle.toml project file"
|
||||||
|
);
|
||||||
|
|
||||||
|
if !cuddle_file.exists() {
|
||||||
|
anyhow::bail!("no cuddle.toml project file found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cuddle_plan_file = tokio::fs::read_to_string(cuddle_file).await?;
|
||||||
|
|
||||||
|
Self::from_file(&cuddle_plan_file, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct RawPlanConfig {
|
||||||
|
pub plan: RawPlanConfigSection,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct RawPlanConfigSection {
|
||||||
|
pub schema: Option<RawPlanSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum RawPlanSchema {
|
||||||
|
Nickel { nickel: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Plan {}
|
||||||
|
impl Plan {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clone_from_project(
|
||||||
|
&self,
|
||||||
|
project: &ProjectPlan,
|
||||||
|
) -> anyhow::Result<Option<ClonedPlan>> {
|
||||||
|
if !project.plan_path().exists() {
|
||||||
|
if project.has_plan() {
|
||||||
|
self.prepare_plan(project).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match project.get_plan() {
|
||||||
|
project::Plan::None => Ok(None),
|
||||||
|
project::Plan::Git(url) => Ok(Some(self.git_plan(project, url).await?)),
|
||||||
|
project::Plan::Folder(folder) => {
|
||||||
|
Ok(Some(self.folder_plan(project, &folder).await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match project.get_plan() {
|
||||||
|
project::Plan::Folder(folder) => {
|
||||||
|
self.clean_plan(project).await?;
|
||||||
|
self.prepare_plan(project).await?;
|
||||||
|
|
||||||
|
Ok(Some(self.folder_plan(project, &folder).await?))
|
||||||
|
}
|
||||||
|
project::Plan::Git(_git) => Ok(Some(ClonedPlan {})),
|
||||||
|
project::Plan::None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> {
|
||||||
|
tracing::trace!("preparing workspace");
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(project.plan_path()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> {
|
||||||
|
tracing::trace!("clean plan");
|
||||||
|
|
||||||
|
tokio::fs::remove_dir_all(project.plan_path()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn git_plan(&self, project: &ProjectPlan, url: String) -> anyhow::Result<ClonedPlan> {
|
||||||
|
let mut cmd = tokio::process::Command::new("git");
|
||||||
|
cmd.args(["clone", &url, &project.plan_path().display().to_string()]);
|
||||||
|
|
||||||
|
tracing::debug!(url = url, "cloning git plan");
|
||||||
|
|
||||||
|
let output = cmd.output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"failed to clone: {}, output: {} {}",
|
||||||
|
url,
|
||||||
|
std::str::from_utf8(&output.stdout)?,
|
||||||
|
std::str::from_utf8(&output.stderr)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ClonedPlan {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn folder_plan(&self, project: &ProjectPlan, path: &Path) -> anyhow::Result<ClonedPlan> {
|
||||||
|
tracing::trace!(
|
||||||
|
src = path.display().to_string(),
|
||||||
|
dest = project.plan_path().display().to_string(),
|
||||||
|
"copying src into plan dest"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut items_stream = tokio::fs::read_dir(path).await?;
|
||||||
|
let mut items = Vec::new();
|
||||||
|
while let Some(item) = items_stream.next_entry().await? {
|
||||||
|
items.push(item.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs_extra::copy_items(
|
||||||
|
&items,
|
||||||
|
project.plan_path(),
|
||||||
|
&CopyOptions::default()
|
||||||
|
.overwrite(true)
|
||||||
|
.depth(0)
|
||||||
|
.copy_inside(false),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ClonedPlan {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClonedPlan {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_parse_schema_plan() -> anyhow::Result<()> {
|
||||||
|
let plan = RawPlan::from_file(
|
||||||
|
r##"
|
||||||
|
[plan]
|
||||||
|
schema = {nickel = "contract.ncl"}
|
||||||
|
"##,
|
||||||
|
&PathBuf::new(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
RawPlanConfig {
|
||||||
|
plan: RawPlanConfigSection {
|
||||||
|
schema: Some(RawPlanSchema::Nickel {
|
||||||
|
nickel: "contract.ncl".into()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plan.config,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
181
crates/cuddle/src/project.rs
Normal file
181
crates/cuddle/src/project.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use std::{
|
||||||
|
env::current_dir,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub const CUDDLE_PROJECT_FILE: &str = "cuddle.toml";
|
||||||
|
|
||||||
|
pub struct RawProject {
|
||||||
|
pub config: RawConfig,
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawProject {
|
||||||
|
pub fn new(config: RawConfig, root: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
root: root.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
|
||||||
|
let config: RawConfig = toml::from_str(content)?;
|
||||||
|
|
||||||
|
Ok(Self::new(config, root))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let cuddle_file = path.join(CUDDLE_PROJECT_FILE);
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
path = cuddle_file.display().to_string(),
|
||||||
|
"searching for cuddle.toml project file"
|
||||||
|
);
|
||||||
|
|
||||||
|
if !cuddle_file.exists() {
|
||||||
|
anyhow::bail!("no cuddle.toml project file found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?;
|
||||||
|
|
||||||
|
Self::from_file(&cuddle_project_file, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProjectPlan {
|
||||||
|
config: ProjectPlanConfig,
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectPlan {
|
||||||
|
pub fn new(config: ProjectPlanConfig, root: PathBuf) -> Self {
|
||||||
|
Self { config, root }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(content: &str, root: PathBuf) -> anyhow::Result<Self> {
|
||||||
|
let config: ProjectPlanConfig = toml::from_str(content)?;
|
||||||
|
|
||||||
|
Ok(Self::new(config, root))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_current_path() -> anyhow::Result<Option<Self>> {
|
||||||
|
let cur_dir = current_dir()?;
|
||||||
|
let cuddle_file = cur_dir.join(CUDDLE_PROJECT_FILE);
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
path = cuddle_file.display().to_string(),
|
||||||
|
"searching for cuddle.toml project file"
|
||||||
|
);
|
||||||
|
|
||||||
|
if !cuddle_file.exists() {
|
||||||
|
tracing::debug!("no cuddle.toml project file found");
|
||||||
|
// We may want to recursively search for the file (towards root)
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?;
|
||||||
|
|
||||||
|
Ok(Some(Self::from_file(&cuddle_project_file, cur_dir)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_plan(&self) -> bool {
|
||||||
|
self.config.plan.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plan(&self) -> Plan {
|
||||||
|
match &self.config.plan {
|
||||||
|
Some(PlanConfig::Bare(git)) | Some(PlanConfig::Git { git }) => Plan::Git(git.clone()),
|
||||||
|
Some(PlanConfig::Folder { path }) => Plan::Folder(path.clone()),
|
||||||
|
None => Plan::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Plan {
|
||||||
|
None,
|
||||||
|
Git(String),
|
||||||
|
Folder(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct RawConfig {
|
||||||
|
project: ProjectConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct ProjectConfig {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct ProjectPlanConfig {
|
||||||
|
plan: Option<PlanConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PlanConfig {
|
||||||
|
Bare(String),
|
||||||
|
Git { git: String },
|
||||||
|
Folder { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_parse_simple_file() -> anyhow::Result<()> {
|
||||||
|
let project = ProjectPlan::from_file(
|
||||||
|
r##"
|
||||||
|
[plan]
|
||||||
|
git = "https://github.com/kjuulh/some-cuddle-project"
|
||||||
|
"##,
|
||||||
|
PathBuf::new(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ProjectPlanConfig {
|
||||||
|
plan: Some(PlanConfig::Git {
|
||||||
|
git: "https://github.com/kjuulh/some-cuddle-project".into()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
project.config
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_parse_simple_file_bare() -> anyhow::Result<()> {
|
||||||
|
let project = ProjectPlan::from_file(
|
||||||
|
r##"
|
||||||
|
plan = "https://github.com/kjuulh/some-cuddle-project"
|
||||||
|
"##,
|
||||||
|
PathBuf::new(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ProjectPlanConfig {
|
||||||
|
plan: Some(PlanConfig::Bare(
|
||||||
|
"https://github.com/kjuulh/some-cuddle-project".into()
|
||||||
|
))
|
||||||
|
},
|
||||||
|
project.config
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_parse_simple_file_none() -> anyhow::Result<()> {
|
||||||
|
let project = ProjectPlan::from_file(r##""##, PathBuf::new())?;
|
||||||
|
|
||||||
|
assert_eq!(ProjectPlanConfig { plan: None }, project.config);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
29
crates/cuddle/src/schema_validator.rs
Normal file
29
crates/cuddle/src/schema_validator.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use nickel::NickelSchemaValidator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
plan::{RawPlan, RawPlanSchema},
|
||||||
|
project::RawProject,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod nickel;
|
||||||
|
|
||||||
|
pub struct SchemaValidator {}
|
||||||
|
|
||||||
|
impl SchemaValidator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self, plan: &RawPlan, project: &RawProject) -> anyhow::Result<Option<()>> {
|
||||||
|
let schema = match &plan.config.plan.schema {
|
||||||
|
Some(schema) => schema,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
match schema {
|
||||||
|
RawPlanSchema::Nickel { nickel } => Ok(Some(NickelSchemaValidator::validate(
|
||||||
|
plan, project, nickel,
|
||||||
|
)?)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
crates/cuddle/src/schema_validator/nickel.rs
Normal file
111
crates/cuddle/src/schema_validator/nickel.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use std::{
|
||||||
|
env::temp_dir,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
plan::RawPlan,
|
||||||
|
project::{RawProject, CUDDLE_PROJECT_FILE},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait NickelPlanExt {
|
||||||
|
fn schema_path(&self, schema: &Path) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NickelPlanExt for RawPlan {
|
||||||
|
fn schema_path(&self, schema: &Path) -> PathBuf {
|
||||||
|
self.root.join(schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NickelProjectExt {
|
||||||
|
fn project_path(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NickelProjectExt for RawProject {
|
||||||
|
fn project_path(&self) -> PathBuf {
|
||||||
|
self.root.join(CUDDLE_PROJECT_FILE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_contract_file() -> anyhow::Result<TempDirGuard> {
|
||||||
|
let p = temp_dir()
|
||||||
|
.join("cuddle")
|
||||||
|
.join("nickel-contracts")
|
||||||
|
.join(Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&p)?;
|
||||||
|
|
||||||
|
let file = p.join("contract.ncl");
|
||||||
|
|
||||||
|
Ok(TempDirGuard { dir: p, file })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TempDirGuard {
|
||||||
|
dir: PathBuf,
|
||||||
|
file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDirGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&self.dir) {
|
||||||
|
panic!("failed to remove tempdir: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TempDirGuard {
|
||||||
|
type Target = PathBuf;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NickelSchemaValidator {}
|
||||||
|
impl NickelSchemaValidator {
|
||||||
|
pub fn validate(plan: &RawPlan, project: &RawProject, nickel: &Path) -> anyhow::Result<()> {
|
||||||
|
let nickel_file = plan.schema_path(nickel);
|
||||||
|
let cuddle_file = project.project_path();
|
||||||
|
|
||||||
|
let nickel_file = format!(
|
||||||
|
r##"
|
||||||
|
let {{ProjectSchema, ..}} = import "{}" in
|
||||||
|
|
||||||
|
let Schema = {{
|
||||||
|
project | ProjectSchema, ..
|
||||||
|
}} in
|
||||||
|
|
||||||
|
{{
|
||||||
|
|
||||||
|
config | Schema = import "{}"
|
||||||
|
}}
|
||||||
|
|
||||||
|
"##,
|
||||||
|
nickel_file.display(),
|
||||||
|
cuddle_file.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let contract_file = unique_contract_file()?;
|
||||||
|
|
||||||
|
std::fs::write(contract_file.as_path(), nickel_file)?;
|
||||||
|
|
||||||
|
let mut cmd = std::process::Command::new("nickel");
|
||||||
|
|
||||||
|
cmd.args(["export", &contract_file.display().to_string()]);
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"failed to run nickel command: output: {} {}",
|
||||||
|
std::str::from_utf8(&output.stdout)?,
|
||||||
|
std::str::from_utf8(&output.stderr)?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
60
crates/cuddle/src/state.rs
Normal file
60
crates/cuddle/src/state.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use validated_project::Project;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
plan::{self, ClonedPlan, PlanPathExt},
|
||||||
|
project::{self, ProjectPlan},
|
||||||
|
schema_validator::SchemaValidator,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod validated_project;
|
||||||
|
|
||||||
|
pub struct State {}
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_state(
|
||||||
|
&self,
|
||||||
|
project_plan: &ProjectPlan,
|
||||||
|
cloned_plan: &Option<ClonedPlan>,
|
||||||
|
) -> anyhow::Result<RawState> {
|
||||||
|
let project = project::RawProject::from_path(&project_plan.root).await?;
|
||||||
|
let plan = if let Some(_cloned_plan) = cloned_plan {
|
||||||
|
Some(plan::RawPlan::from_path(&project_plan.plan_path()).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RawState { project, plan })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_state(&self, state: &RawState) -> anyhow::Result<ValidatedState> {
|
||||||
|
// 2. Prepare context for actions and components
|
||||||
|
|
||||||
|
if let Some(plan) = &state.plan {
|
||||||
|
SchemaValidator::new().validate(plan, &state.project)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match against schema from plan
|
||||||
|
let project = validated_project::Project::from_path(&state.project.root).await?;
|
||||||
|
|
||||||
|
Ok(ValidatedState {
|
||||||
|
project: Some(project),
|
||||||
|
plan: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RawState {
|
||||||
|
project: project::RawProject,
|
||||||
|
plan: Option<plan::RawPlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ValidatedState {
|
||||||
|
pub project: Option<Project>,
|
||||||
|
pub plan: Option<Plan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Plan {}
|
75
crates/cuddle/src/state/validated_project.rs
Normal file
75
crates/cuddle/src/state/validated_project.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use serde::Serialize;
|
||||||
|
use toml::Table;
|
||||||
|
|
||||||
|
use crate::project::CUDDLE_PROJECT_FILE;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Project {
|
||||||
|
pub value: Value,
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
pub fn new(value: Value, root: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
root: root.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
|
||||||
|
let table: Table = toml::from_str(content)?;
|
||||||
|
|
||||||
|
let project = table
|
||||||
|
.get("project")
|
||||||
|
.ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?;
|
||||||
|
|
||||||
|
let value: Value = project.into();
|
||||||
|
let value = Value::Map([("project".to_string(), value)].into());
|
||||||
|
|
||||||
|
Ok(Self::new(value, root))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let cuddle_file = path.join(CUDDLE_PROJECT_FILE);
|
||||||
|
|
||||||
|
if !cuddle_file.exists() {
|
||||||
|
anyhow::bail!("no cuddle.toml project file found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?;
|
||||||
|
|
||||||
|
Self::from_file(&cuddle_project_file, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Value {
|
||||||
|
String(String),
|
||||||
|
Bool(bool),
|
||||||
|
Array(Vec<Value>),
|
||||||
|
Map(BTreeMap<String, Value>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&toml::Value> for Value {
|
||||||
|
fn from(value: &toml::Value) -> Self {
|
||||||
|
match value {
|
||||||
|
toml::Value::String(s) => Self::String(s.clone()),
|
||||||
|
toml::Value::Integer(i) => Self::String(i.to_string()),
|
||||||
|
toml::Value::Float(f) => Self::String(f.to_string()),
|
||||||
|
toml::Value::Boolean(b) => Self::Bool(*b),
|
||||||
|
toml::Value::Datetime(dt) => Self::String(dt.to_string()),
|
||||||
|
toml::Value::Array(array) => Self::Array(array.iter().map(|i| i.into()).collect()),
|
||||||
|
toml::Value::Table(tbl) => {
|
||||||
|
Self::Map(tbl.iter().map(|(k, v)| (k.clone(), v.into())).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
cuddle.yaml
21
cuddle.yaml
@@ -1,18 +1,15 @@
|
|||||||
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
|
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
|
||||||
|
|
||||||
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
|
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-cli-plan.git"
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
service: "cuddle-image"
|
service: "cuddle"
|
||||||
registry: kasperhermansen
|
registry: kasperhermansen
|
||||||
|
|
||||||
scripts:
|
please:
|
||||||
build_cuddle_image:
|
project:
|
||||||
type: shell
|
owner: kjuulh
|
||||||
args:
|
repository: "cuddle"
|
||||||
docker_username:
|
branch: "main"
|
||||||
key: "DOCKER_USERNAME"
|
settings:
|
||||||
type: env
|
api_url: "https://git.front.kjuulh.io"
|
||||||
docker_password:
|
|
||||||
key: "DOCKER_PASSWORD"
|
|
||||||
type: env
|
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "cuddle_cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.60"
|
|
||||||
serde = { version = "1.0.143", features = ["derive"] }
|
|
||||||
serde_yaml = "0.9.4"
|
|
||||||
walkdir = "2.3.2"
|
|
||||||
git2 = { version = "0.15.0", features = ["ssh"] }
|
|
||||||
clap = "3.2.16"
|
|
||||||
envconfig = "0.10.0"
|
|
||||||
dirs = "4.0.0"
|
|
||||||
tracing = "0.1.36"
|
|
||||||
tracing-subscriber = { version = "0.3.15", features = ["json"] }
|
|
||||||
log = { version = "0.4.17", features = ["std", "kv_unstable"] }
|
|
||||||
openssl = {version = "0.10", features = ["vendored"]}
|
|
||||||
tera = "1.17.0"
|
|
@@ -1,78 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
actions::shell::ShellAction,
|
|
||||||
model::{CuddleScript, CuddleShellScriptArg, CuddleVariable},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod shell;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct CuddleAction {
|
|
||||||
pub script: CuddleScript,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl CuddleAction {
|
|
||||||
pub fn new(script: CuddleScript, path: PathBuf, name: String) -> Self {
|
|
||||||
Self { script, path, name }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
|
|
||||||
match self.script {
|
|
||||||
CuddleScript::Shell(s) => {
|
|
||||||
let mut arg_variables: Vec<CuddleVariable> = vec![];
|
|
||||||
if let Some(args) = s.args {
|
|
||||||
for (k, v) in args {
|
|
||||||
let var = match v {
|
|
||||||
CuddleShellScriptArg::Env(e) => {
|
|
||||||
let env_var = match std::env::var(e.key.clone()) {
|
|
||||||
Ok(var) => var,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("env_variable not found: {}", k);
|
|
||||||
return Err(anyhow::anyhow!(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
CuddleVariable::new(k.clone(), env_var)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
arg_variables.push(var);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
arg_variables = vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut vars = variables.clone();
|
|
||||||
vars.append(&mut arg_variables);
|
|
||||||
|
|
||||||
log::trace!("preparing to run action");
|
|
||||||
|
|
||||||
match ShellAction::new(
|
|
||||||
self.name.clone(),
|
|
||||||
format!(
|
|
||||||
"{}/scripts/{}.sh",
|
|
||||||
self.path
|
|
||||||
.to_str()
|
|
||||||
.expect("action doesn't have a name, this should never happen"),
|
|
||||||
self.name
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute(vars)
|
|
||||||
{
|
|
||||||
Ok(()) => {
|
|
||||||
log::trace!("finished running action");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
use std::{env::current_dir, path::PathBuf, process::Command};
|
|
||||||
|
|
||||||
use crate::model::CuddleVariable;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct ShellAction {
|
|
||||||
path: String,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShellAction {
|
|
||||||
pub fn new(name: String, path: String) -> Self {
|
|
||||||
Self { path, name }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
|
|
||||||
log::debug!("executing shell action: {}", self.path.clone());
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"
|
|
||||||
|
|
||||||
===
|
|
||||||
Starting running shell action: {}
|
|
||||||
===
|
|
||||||
",
|
|
||||||
self.path.clone()
|
|
||||||
);
|
|
||||||
|
|
||||||
let path = PathBuf::from(self.path.clone());
|
|
||||||
if !path.exists() {
|
|
||||||
log::info!("script='{}' not found, aborting", path.to_string_lossy());
|
|
||||||
return Err(anyhow::anyhow!("file not found aborting"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_dir = current_dir()?;
|
|
||||||
log::trace!("current executable dir={}", current_dir.to_string_lossy());
|
|
||||||
|
|
||||||
let mut process = Command::new(path)
|
|
||||||
.current_dir(current_dir)
|
|
||||||
.envs(variables.iter().map(|v| {
|
|
||||||
log::trace!("{:?}", v);
|
|
||||||
|
|
||||||
(v.name.to_uppercase(), v.value.clone())
|
|
||||||
}))
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
process.wait()?;
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"
|
|
||||||
|
|
||||||
===
|
|
||||||
Finished running shell action
|
|
||||||
===
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,193 +0,0 @@
|
|||||||
mod subcommands;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::Command;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
actions::CuddleAction,
|
|
||||||
config::{CuddleConfig, CuddleFetchPolicy},
|
|
||||||
context::{CuddleContext, CuddleTreeType},
|
|
||||||
model::*,
|
|
||||||
util::git::GitCommit,
|
|
||||||
};
|
|
||||||
|
|
||||||
use self::subcommands::render_template::RenderTemplateCommand;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CuddleCli<'a> {
|
|
||||||
scripts: Vec<CuddleAction>,
|
|
||||||
variables: Vec<CuddleVariable>,
|
|
||||||
context: Arc<Mutex<Vec<CuddleContext>>>,
|
|
||||||
command: Option<Command<'a>>,
|
|
||||||
tmp_dir: Option<PathBuf>,
|
|
||||||
config: CuddleConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> CuddleCli<'a> {
|
|
||||||
pub fn new(
|
|
||||||
context: Arc<Mutex<Vec<CuddleContext>>>,
|
|
||||||
config: CuddleConfig,
|
|
||||||
) -> anyhow::Result<CuddleCli<'a>> {
|
|
||||||
let mut cli = CuddleCli {
|
|
||||||
scripts: vec![],
|
|
||||||
variables: vec![],
|
|
||||||
context: context.clone(),
|
|
||||||
command: None,
|
|
||||||
tmp_dir: None,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
|
|
||||||
cli = cli
|
|
||||||
.process_variables()
|
|
||||||
.process_scripts()
|
|
||||||
.process_templates()?
|
|
||||||
.build_cli();
|
|
||||||
|
|
||||||
Ok(cli)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_variables(mut self) -> Self {
|
|
||||||
if let Ok(context_iter) = self.context.clone().lock() {
|
|
||||||
for ctx in context_iter.iter() {
|
|
||||||
if let Some(variables) = ctx.plan.vars.clone() {
|
|
||||||
for (name, var) in variables {
|
|
||||||
self.variables.push(CuddleVariable::new(name, var))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let CuddleTreeType::Root = ctx.node_type {
|
|
||||||
let mut temp_path = ctx.path.clone();
|
|
||||||
temp_path.push(".cuddle/tmp");
|
|
||||||
|
|
||||||
self.variables.push(CuddleVariable::new(
|
|
||||||
"tmp".into(),
|
|
||||||
temp_path.clone().to_string_lossy().to_string(),
|
|
||||||
));
|
|
||||||
|
|
||||||
self.tmp_dir = Some(temp_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match GitCommit::new() {
|
|
||||||
Ok(commit) => self.variables.push(CuddleVariable::new(
|
|
||||||
"commit_sha".into(),
|
|
||||||
commit.commit_sha.clone(),
|
|
||||||
)),
|
|
||||||
Err(e) => {
|
|
||||||
log::debug!("{}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_scripts(mut self) -> Self {
|
|
||||||
if let Ok(context_iter) = self.context.clone().lock() {
|
|
||||||
for ctx in context_iter.iter() {
|
|
||||||
if let Some(scripts) = ctx.plan.scripts.clone() {
|
|
||||||
for (name, script) in scripts {
|
|
||||||
self.scripts
|
|
||||||
.push(CuddleAction::new(script.clone(), ctx.path.clone(), name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_templates(self) -> anyhow::Result<Self> {
|
|
||||||
if let None = self.tmp_dir {
|
|
||||||
log::debug!("cannot process template as bare bones cli");
|
|
||||||
return Ok(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure tmp_dir exists and clean it up first
|
|
||||||
let tmp_dir = self
|
|
||||||
.tmp_dir
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("tmp_dir does not exist aborting"))?;
|
|
||||||
match self.config.get_fetch_policy()? {
|
|
||||||
CuddleFetchPolicy::Always => {
|
|
||||||
if tmp_dir.exists() && tmp_dir.ends_with("tmp") {
|
|
||||||
std::fs::remove_dir_all(tmp_dir.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
std::fs::create_dir_all(tmp_dir.clone())?;
|
|
||||||
// Handle all templating with variables and such.
|
|
||||||
// TODO: use actual templating engine, for new we just copy templates to the final folder
|
|
||||||
|
|
||||||
if let Ok(context_iter) = self.context.clone().lock() {
|
|
||||||
for ctx in context_iter.iter() {
|
|
||||||
let mut template_path = ctx.path.clone();
|
|
||||||
template_path.push("templates");
|
|
||||||
|
|
||||||
log::trace!("template path: {}", template_path.clone().to_string_lossy());
|
|
||||||
|
|
||||||
if !template_path.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for file in std::fs::read_dir(template_path)?.into_iter() {
|
|
||||||
let f = file?;
|
|
||||||
let mut dest_file = tmp_dir.clone();
|
|
||||||
dest_file.push(f.file_name());
|
|
||||||
|
|
||||||
std::fs::copy(f.path(), dest_file)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_cli(mut self) -> Self {
|
|
||||||
let mut root_cmd = Command::new("cuddle")
|
|
||||||
.version("1.0")
|
|
||||||
.author("kjuulh <contact@kasperhermansen.com>")
|
|
||||||
.about("cuddle is your domain specific organization tool. It enabled widespread sharing through repositories, as well as collaborating while maintaining speed and integrity")
|
|
||||||
.subcommand_required(true)
|
|
||||||
.arg_required_else_help(true)
|
|
||||||
.propagate_version(true);
|
|
||||||
|
|
||||||
root_cmd = subcommands::x::build_command(root_cmd, self.clone());
|
|
||||||
root_cmd = subcommands::render_template::build_command(root_cmd);
|
|
||||||
|
|
||||||
self.command = Some(root_cmd);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(self) -> anyhow::Result<Self> {
|
|
||||||
if let Some(mut cli) = self.command.clone() {
|
|
||||||
let matches = cli.clone().get_matches();
|
|
||||||
|
|
||||||
let res = match matches.subcommand() {
|
|
||||||
Some(("x", exe_submatch)) => subcommands::x::execute_x(exe_submatch, self.clone()),
|
|
||||||
Some(("render_template", sub_matches)) => {
|
|
||||||
RenderTemplateCommand::from_matches(sub_matches, self.clone())
|
|
||||||
.and_then(|cmd| cmd.execute())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
|
||||||
};
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(e) => {
|
|
||||||
let _ = cli.print_long_help();
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,2 +0,0 @@
|
|||||||
pub mod render_template;
|
|
||||||
pub mod x;
|
|
@@ -1,150 +0,0 @@
|
|||||||
use std::{path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
use clap::{Arg, ArgMatches, Command};
|
|
||||||
|
|
||||||
use crate::{cli::CuddleCli, model::CuddleVariable};
|
|
||||||
|
|
||||||
pub fn build_command<'a>(root_cmd: Command<'a>) -> Command<'a> {
|
|
||||||
root_cmd.subcommand(
|
|
||||||
Command::new("render_template")
|
|
||||||
.about("renders a jinja compatible template")
|
|
||||||
.args(&[
|
|
||||||
Arg::new("template-file")
|
|
||||||
.alias("template")
|
|
||||||
.short('t')
|
|
||||||
.long("template-file")
|
|
||||||
.required(true)
|
|
||||||
.action(clap::ArgAction::Set).long_help("template-file is the input file path of the .tmpl file (or inferred) that you would like to render"),
|
|
||||||
Arg::new("destination")
|
|
||||||
.alias("dest")
|
|
||||||
.short('d')
|
|
||||||
.long("destination")
|
|
||||||
.required(true)
|
|
||||||
.action(clap::ArgAction::Set)
|
|
||||||
.long_help("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)"),
|
|
||||||
Arg::new("extra-var")
|
|
||||||
.long("extra-var")
|
|
||||||
.required(false)
|
|
||||||
.action(clap::ArgAction::Set),
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RenderTemplateCommand {
|
|
||||||
variables: Vec<CuddleVariable>,
|
|
||||||
template_file: PathBuf,
|
|
||||||
destination: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderTemplateCommand {
|
|
||||||
pub fn from_matches(matches: &ArgMatches, cli: CuddleCli) -> anyhow::Result<Self> {
|
|
||||||
let template_file = matches
|
|
||||||
.get_one::<String>("template-file")
|
|
||||||
.ok_or(anyhow::anyhow!("template-file was not found"))
|
|
||||||
.and_then(get_path_buf_and_check_exists)?;
|
|
||||||
|
|
||||||
let destination = matches
|
|
||||||
.get_one::<String>("destination")
|
|
||||||
.ok_or(anyhow::anyhow!("destination was not found"))
|
|
||||||
.and_then(get_path_buf_and_check_dir_exists)
|
|
||||||
.and_then(RenderTemplateCommand::transform_extension)?;
|
|
||||||
|
|
||||||
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].into(), parts[1].into()));
|
|
||||||
}
|
|
||||||
vars
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
extra_vars.append(&mut cli.variables.clone());
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
variables: extra_vars,
|
|
||||||
template_file,
|
|
||||||
destination,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(self) -> anyhow::Result<()> {
|
|
||||||
// Prepare context
|
|
||||||
let mut context = tera::Context::new();
|
|
||||||
for var in self.variables {
|
|
||||||
context.insert(
|
|
||||||
var.name.to_lowercase().replace(" ", "_").replace("-", "_"),
|
|
||||||
&var.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load source template
|
|
||||||
let source = std::fs::read_to_string(self.template_file)?;
|
|
||||||
|
|
||||||
let output = tera::Tera::one_off(source.as_str(), &context, false)?;
|
|
||||||
|
|
||||||
// Put template in final destination
|
|
||||||
std::fs::write(&self.destination, output)?;
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"finished writing template to: {}",
|
|
||||||
&self.destination.to_string_lossy()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transform_extension(template_path: PathBuf) -> anyhow::Result<PathBuf> {
|
|
||||||
if template_path.is_file() {
|
|
||||||
let ext = template_path.extension().ok_or(anyhow::anyhow!(
|
|
||||||
"destination path does not have an extension"
|
|
||||||
))?;
|
|
||||||
if ext.to_string_lossy().ends_with("tmpl") {
|
|
||||||
let template_dest = template_path
|
|
||||||
.to_str()
|
|
||||||
.and_then(|s| s.strip_suffix(".tmpl"))
|
|
||||||
.ok_or(anyhow::anyhow!("string does not end in .tmpl"))?;
|
|
||||||
|
|
||||||
return PathBuf::from_str(template_dest).map_err(|e| anyhow::anyhow!(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(template_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_path_buf_and_check_exists(raw_path: &String) -> anyhow::Result<PathBuf> {
|
|
||||||
match PathBuf::from_str(&raw_path) {
|
|
||||||
Ok(pb) => {
|
|
||||||
if pb.exists() {
|
|
||||||
Ok(pb)
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"path: {}, could not be found",
|
|
||||||
pb.to_string_lossy()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(anyhow::anyhow!(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_path_buf_and_check_dir_exists(raw_path: &String) -> anyhow::Result<PathBuf> {
|
|
||||||
match PathBuf::from_str(&raw_path) {
|
|
||||||
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)),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
use clap::{ArgMatches, Command};
|
|
||||||
|
|
||||||
use crate::cli::CuddleCli;
|
|
||||||
|
|
||||||
pub fn build_command<'a>(root_cmd: Command<'a>, cli: CuddleCli<'a>) -> Command<'a> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
for script in cli.scripts.iter() {
|
|
||||||
let action_cmd = Command::new(script.name.clone());
|
|
||||||
|
|
||||||
// TODO: Some way to add an about for clap, requires conversion from String -> &str
|
|
||||||
|
|
||||||
execute_cmd = execute_cmd.subcommand(action_cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
root_cmd.subcommand(execute_cmd)
|
|
||||||
} else {
|
|
||||||
root_cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_x(exe_submatch: &ArgMatches, cli: CuddleCli) -> anyhow::Result<()> {
|
|
||||||
match exe_submatch.subcommand() {
|
|
||||||
Some((name, _action_matches)) => {
|
|
||||||
log::trace!(action=name; "running action; name={}", name);
|
|
||||||
match cli.scripts.iter().find(|ele| ele.name == name) {
|
|
||||||
Some(script) => {
|
|
||||||
script.clone().execute(cli.variables.clone())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
use envconfig::Envconfig;
|
|
||||||
|
|
||||||
pub enum CuddleFetchPolicy {
|
|
||||||
Always,
|
|
||||||
Once,
|
|
||||||
Never,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Envconfig, Clone, Debug)]
|
|
||||||
pub struct CuddleConfig {
|
|
||||||
#[envconfig(from = "CUDDLE_FETCH_POLICY", default = "once")]
|
|
||||||
fetch_policy: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CuddleConfig {
|
|
||||||
pub fn from_env() -> anyhow::Result<Self> {
|
|
||||||
CuddleConfig::init_from_env().map_err(|e| anyhow::Error::from(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_fetch_policy(&self) -> anyhow::Result<CuddleFetchPolicy> {
|
|
||||||
match self.fetch_policy.clone().to_lowercase().as_str() {
|
|
||||||
"always" => Ok(CuddleFetchPolicy::Always),
|
|
||||||
"once" => Ok(CuddleFetchPolicy::Once),
|
|
||||||
"never" => Ok(CuddleFetchPolicy::Never),
|
|
||||||
_ => Err(anyhow::anyhow!("could not parse fetch policy")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,192 +0,0 @@
|
|||||||
use std::{
|
|
||||||
env::{self, current_dir},
|
|
||||||
ffi::OsStr,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::{CuddleConfig, CuddleFetchPolicy},
|
|
||||||
model::{CuddleBase, CuddlePlan},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum CuddleTreeType {
|
|
||||||
Root,
|
|
||||||
Leaf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CuddleContext {
|
|
||||||
pub plan: CuddlePlan,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub node_type: CuddleTreeType,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_cuddle(config: CuddleConfig) -> anyhow::Result<Arc<Mutex<Vec<CuddleContext>>>> {
|
|
||||||
let mut curr_dir = current_dir()?;
|
|
||||||
curr_dir.push(".cuddle/");
|
|
||||||
let fetch_policy = config.get_fetch_policy()?;
|
|
||||||
if let CuddleFetchPolicy::Always = fetch_policy {
|
|
||||||
if curr_dir.exists() {
|
|
||||||
if let Err(res) = std::fs::remove_dir_all(curr_dir) {
|
|
||||||
panic!("{}", res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load main cuddle file.
|
|
||||||
let cuddle_yaml = find_root_cuddle()?;
|
|
||||||
log::trace!(cuddle_yaml=log::as_debug!(cuddle_yaml); "Find root cuddle");
|
|
||||||
|
|
||||||
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(cuddle_yaml.as_str())?;
|
|
||||||
log::debug!(cuddle_plan=log::as_debug!(cuddle_yaml); "parse cuddle plan");
|
|
||||||
|
|
||||||
let context: Arc<Mutex<Vec<CuddleContext>>> = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
context.lock().unwrap().push(CuddleContext {
|
|
||||||
plan: cuddle_plan.clone(),
|
|
||||||
path: current_dir()?,
|
|
||||||
node_type: CuddleTreeType::Root,
|
|
||||||
});
|
|
||||||
|
|
||||||
// pull parent plan and execute recursive descent
|
|
||||||
match cuddle_plan.base {
|
|
||||||
CuddleBase::Bool(true) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"plan cannot be enabled without specifying a plan"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
CuddleBase::Bool(false) => {
|
|
||||||
log::debug!("plan is root: skipping");
|
|
||||||
}
|
|
||||||
CuddleBase::String(parent_plan) => {
|
|
||||||
let destination_path = create_cuddle_local()?;
|
|
||||||
let mut cuddle_dest = destination_path.clone();
|
|
||||||
cuddle_dest.push("base");
|
|
||||||
|
|
||||||
if !cuddle_dest.exists() {
|
|
||||||
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
recurse_parent(cuddle_dest, context.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cuddle_local() -> anyhow::Result<PathBuf> {
|
|
||||||
let mut curr_dir = current_dir()?.clone();
|
|
||||||
curr_dir.push(".cuddle/");
|
|
||||||
|
|
||||||
if curr_dir.exists() {
|
|
||||||
log::debug!(".cuddle/ already exists: skipping");
|
|
||||||
return Ok(curr_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::create_dir(curr_dir.clone())?;
|
|
||||||
|
|
||||||
Ok(curr_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cuddle(path: PathBuf) -> anyhow::Result<PathBuf> {
|
|
||||||
let mut curr_dir = path.clone();
|
|
||||||
curr_dir.push(".cuddle/");
|
|
||||||
|
|
||||||
if curr_dir.exists() {
|
|
||||||
log::debug!(".cuddle/ already exists: skipping");
|
|
||||||
return Ok(curr_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::create_dir(curr_dir.clone())?;
|
|
||||||
|
|
||||||
Ok(curr_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pull_parent_cuddle_into_local(
|
|
||||||
parent_cuddle: String,
|
|
||||||
destination: PathBuf,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut rc = RemoteCallbacks::new();
|
|
||||||
rc.credentials(|_url, username_from_url, _allowed_types| {
|
|
||||||
git2::Cred::ssh_key(
|
|
||||||
username_from_url.unwrap(),
|
|
||||||
None,
|
|
||||||
Path::new(&format!("{}/.ssh/id_ed25519", env::var("HOME").unwrap())),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut fo = FetchOptions::new();
|
|
||||||
fo.remote_callbacks(rc);
|
|
||||||
|
|
||||||
RepoBuilder::new()
|
|
||||||
.fetch_options(fo)
|
|
||||||
.clone(&parent_cuddle, &destination)?;
|
|
||||||
|
|
||||||
log::debug!(parent_cuddle=log::as_display!(parent_cuddle); "pulled repository");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recurse_parent(path: PathBuf, context: Arc<Mutex<Vec<CuddleContext>>>) -> anyhow::Result<()> {
|
|
||||||
let cuddle_contents = find_cuddle(path.clone())?;
|
|
||||||
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents)?;
|
|
||||||
|
|
||||||
let ctx = context.clone();
|
|
||||||
if let Ok(mut ctxs) = ctx.lock() {
|
|
||||||
ctxs.push(CuddleContext {
|
|
||||||
plan: cuddle_plan.clone(),
|
|
||||||
path: path.clone(),
|
|
||||||
node_type: CuddleTreeType::Leaf,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("Could not acquire lock, aborting"));
|
|
||||||
}
|
|
||||||
|
|
||||||
match cuddle_plan.base {
|
|
||||||
CuddleBase::Bool(true) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"plan cannot be enabled without specifying a plan"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
CuddleBase::Bool(false) => {
|
|
||||||
log::debug!("plan is root: finishing up");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
CuddleBase::String(parent_plan) => {
|
|
||||||
let destination_path = create_cuddle(path.clone())?;
|
|
||||||
let mut cuddle_dest = destination_path.clone();
|
|
||||||
cuddle_dest.push("base");
|
|
||||||
|
|
||||||
if !cuddle_dest.exists() {
|
|
||||||
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
|
|
||||||
}
|
|
||||||
return recurse_parent(cuddle_dest, context.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_root_cuddle() -> anyhow::Result<String> {
|
|
||||||
// TODO: Make recursive towards root
|
|
||||||
let current_dir = env::current_dir()?;
|
|
||||||
find_cuddle(current_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_cuddle(path: PathBuf) -> anyhow::Result<String> {
|
|
||||||
for entry in std::fs::read_dir(path)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
let metadata = std::fs::metadata(&path)?;
|
|
||||||
if metadata.is_file() && path.file_name().unwrap() == OsStr::new("cuddle.yaml") {
|
|
||||||
return Ok(std::fs::read_to_string(path)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Could not find 'cuddle.yaml' in the current directory"
|
|
||||||
))
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
use config::CuddleConfig;
|
|
||||||
use tracing::Level;
|
|
||||||
|
|
||||||
mod actions;
|
|
||||||
mod cli;
|
|
||||||
mod config;
|
|
||||||
mod context;
|
|
||||||
mod model;
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
|
||||||
init_logging()?;
|
|
||||||
|
|
||||||
let config = CuddleConfig::from_env()?;
|
|
||||||
|
|
||||||
let context = context::extract_cuddle(config.clone())?;
|
|
||||||
_ = cli::CuddleCli::new(context, config)?.execute()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_logging() -> anyhow::Result<()> {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.pretty()
|
|
||||||
.with_max_level(Level::INFO)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum CuddleBase {
|
|
||||||
Bool(bool),
|
|
||||||
String(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
pub struct CuddleShellScriptArgEnv {
|
|
||||||
pub key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum CuddleShellScriptArg {
|
|
||||||
#[serde(alias = "env")]
|
|
||||||
Env(CuddleShellScriptArgEnv),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
pub struct CuddleShellScript {
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub args: Option<HashMap<String, CuddleShellScriptArg>>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
pub struct CuddleDaggerScript {
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum CuddleScript {
|
|
||||||
#[serde(alias = "shell")]
|
|
||||||
Shell(CuddleShellScript),
|
|
||||||
#[serde(alias = "dagger")]
|
|
||||||
Dagger(CuddleDaggerScript),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
pub struct CuddlePlan {
|
|
||||||
pub base: CuddleBase,
|
|
||||||
pub vars: Option<HashMap<String, String>>,
|
|
||||||
pub scripts: Option<HashMap<String, CuddleScript>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct CuddleVariable {
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CuddleVariable {
|
|
||||||
pub fn new(name: String, value: String) -> Self {
|
|
||||||
Self { name, value }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
use std::env::current_dir;
|
|
||||||
|
|
||||||
use git2::Repository;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GitCommit {
|
|
||||||
pub commit_sha: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitCommit {
|
|
||||||
pub fn new() -> anyhow::Result<GitCommit> {
|
|
||||||
let repo = Repository::open(current_dir().expect("having current_dir available")).map_err(
|
|
||||||
|e| {
|
|
||||||
log::debug!("{}", e);
|
|
||||||
anyhow::anyhow!("could not open repository")
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
let head_ref = repo
|
|
||||||
.head()
|
|
||||||
.map_err(|e| {
|
|
||||||
log::warn!("{}", e);
|
|
||||||
anyhow::anyhow!("could not get HEAD")
|
|
||||||
})?
|
|
||||||
.target()
|
|
||||||
.ok_or(anyhow::anyhow!(
|
|
||||||
"could not extract head -> target to commit_sha"
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
commit_sha: head_ref.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
pub mod git;
|
|
@@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "base"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
@@ -1,8 +0,0 @@
|
|||||||
# yaml-language-server: $schema=../../schemas/base.json
|
|
||||||
|
|
||||||
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-plan.git"
|
|
||||||
|
|
||||||
scripts:
|
|
||||||
build:
|
|
||||||
type: shell
|
|
||||||
description: "build rust plan"
|
|
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "Ran build"
|
|
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
# yaml-language-server: $schema=../../schemas/base.json
|
|
||||||
|
|
||||||
base: false
|
|
||||||
|
|
||||||
vars:
|
|
||||||
service: "some-service"
|
|
||||||
|
|
||||||
scripts:
|
|
||||||
test_render_template:
|
|
||||||
type: shell
|
|
||||||
args:
|
|
||||||
extravar:
|
|
||||||
type: env
|
|
||||||
key: "HOME"
|
|
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
CUDDLE_FETCH_POLICY=never cuddle_cli render_template \
|
|
||||||
--template-file "$TMP/input.txt.tmpl" \
|
|
||||||
--destination "$TMP/input.txt" \
|
|
||||||
--extra-var "extravar=someextravar"
|
|
@@ -1,3 +0,0 @@
|
|||||||
some {{ service }} name
|
|
||||||
|
|
||||||
- {{ extravar }}
|
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
@@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"base": {
|
|
||||||
"title": "Base url from which to base current cuddle plan on",
|
|
||||||
"description": "Base url from which to construct current cuddle plan, is recursive",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"title": "The url of the parameter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "boolean",
|
|
||||||
"title": "Whether it is enabled or not"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"vars": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "your collection of variables to be available to cuddle",
|
|
||||||
"patternProperties": {
|
|
||||||
"^.*$": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "Scripts the cuddle cli can execute",
|
|
||||||
"description": "Scripts the cuddle cli can execute 'cuddle x my-awesome-script'",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"patternProperties": {
|
|
||||||
"^.*$": {
|
|
||||||
"required": [
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"enum": [
|
|
||||||
"shell",
|
|
||||||
"dagger"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"vars": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "your collection of variables to be available to cuddle",
|
|
||||||
"patternProperties": {
|
|
||||||
"^.*$": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"args": {
|
|
||||||
"title": "arguments to send to the specified script",
|
|
||||||
"type": "object",
|
|
||||||
"patternProperties": {
|
|
||||||
"^.*$": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
"key"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"enum": [
|
|
||||||
"env"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"title": "the environment key to pull arg from",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"base"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"title": "Cuddle base schema"
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
tag="$REGISTRY/$SERVICE:${COMMIT_SHA:0:10}"
|
|
||||||
latest_tag="$REGISTRY/$SERVICE:latest"
|
|
||||||
|
|
||||||
echo "logging in"
|
|
||||||
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
|
||||||
|
|
||||||
echo "building image"
|
|
||||||
DOCKER_BUILDKIT=1 docker build -t "$tag" -t "$latest_tag" -f "$TMP/build_cuddle_image.Dockerfile" .
|
|
||||||
|
|
||||||
echo "pushing image"
|
|
||||||
docker push "$tag"
|
|
||||||
docker push "$latest_tag"
|
|
@@ -1,23 +0,0 @@
|
|||||||
FROM rust:1.62.1-slim-bullseye as base
|
|
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
RUN apt-get update -qq && \
|
|
||||||
apt-get install -y \
|
|
||||||
musl-tools \
|
|
||||||
musl-dev
|
|
||||||
|
|
||||||
RUN update-ca-certificates
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get upgrade -y
|
|
||||||
RUN apt-get install -y -q build-essential curl git
|
|
||||||
|
|
||||||
WORKDIR /app/cuddle/
|
|
||||||
|
|
||||||
RUN cargo install --target x86_64-unknown-linux-musl --git https://git.front.kjuulh.io/kjuulh/cuddle.git cuddle_cli
|
|
||||||
|
|
||||||
FROM docker:stable-dind
|
|
||||||
|
|
||||||
COPY --from=base /usr/local/cargo/bin/ /usr/local/cargo/bin/
|
|
||||||
ENV PATH="${PATH}:/usr/local/cargo/bin"
|
|
||||||
|
|
@@ -1,2 +0,0 @@
|
|||||||
.cuddle/
|
|
||||||
.git/
|
|
15
templates/docker-compose.yaml
Normal file
15
templates/docker-compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
crdb:
|
||||||
|
restart: 'always'
|
||||||
|
image: 'cockroachdb/cockroach:v23.1.14'
|
||||||
|
command: 'start-single-node --advertise-addr 0.0.0.0 --insecure'
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
|
||||||
|
interval: '10s'
|
||||||
|
timeout: '30s'
|
||||||
|
retries: 5
|
||||||
|
start_period: '20s'
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
- '26257:26257'
|
Reference in New Issue
Block a user