Compare commits

165 Commits

Author SHA1 Message Date
d33e42a608 feat: add git setup
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-10-23 22:38:41 +02:00
62a677ffcd feat: working cuddle actions,
All checks were successful
continuous-integration/drone/push Build is passing
although I am not entirely happy with it, as we're missing args, state and project variables

Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-26 22:02:50 +02:00
186f13a16c feat: add actions sdk
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-26 09:05:58 +02:00
02dd805db4 feat: add command get for doing queries
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-25 22:13:50 +02:00
23d68caf71 chore: cleanup before get
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-25 21:08:34 +02:00
b3aedfb411 chore: fix warnings
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-25 17:27:31 +02:00
b01543a8b9 chore: remove lib
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-25 17:20:12 +02:00
5e88ffdbc9 feat: add cli
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-24 15:40:44 +02:00
61db3da695 feat: add validated state for project
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-24 15:16:30 +02:00
1ba6cf79c0 feat: fix minor bugs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-24 14:55:26 +02:00
c2b7e44ea3 feat: add nickel validation
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-24 14:47:18 +02:00
531ea225f3 feat: add basic plan and project clone
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-24 00:45:16 +02:00
6cb65e55c1 refactor: extract file
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-23 23:22:35 +02:00
071cb43510 feat: add basic file
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-23 23:21:53 +02:00
78c86dc39a docs: add docs
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-23 22:48:14 +02:00
5bc831da10 feat: add basic cli
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-23 21:53:25 +02:00
a4213ea61f feat: add bare
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-23 21:51:42 +02:00
bf7a7db868 feat: I dunno why
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-04 20:13:38 +02:00
1d1ac49d0b feat: prefer first variable
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-09 21:08:16 +01:00
8520bcb5b0 fix(deps): update rust crate git2 to 0.18.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-07 04:29:42 +00:00
7b136b1331 feat: enable helm via. kustomize
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-06 21:35:27 +01:00
d0d591dd4f fix(deps): update rust crate tempfile to 3.10.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-05 20:31:33 +00:00
e09e28e8d0 feat: reverse folders
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 20:11:54 +01:00
3a09c68378 feat: reverse
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 20:06:17 +01:00
8f889663b2 fix(deps): update rust crate tokio to 1.36.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-02 12:38:27 +00:00
6d3bdda04d fix(deps): update rust crate dagger-sdk to 0.9.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-01 22:02:11 +00:00
1720f29149 fix(deps): update rust crate eyre to 0.6.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-31 17:20:32 +00:00
dc2a4977d6 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-29 04:14:23 +00:00
52c7f77751 feat: narrow error codes
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:36:26 +01:00
ede55b975b feat: without remove dir all assertion
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 16:54:25 +01:00
85cc1d46db feat: make sure dir is there as well
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 16:42:34 +01:00
cc7aaf14eb fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-26 22:27:07 +00:00
1bacfdfb29 fix(deps): update rust crate libz-sys to 1.1.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-26 14:46:06 +00:00
9820d2c3ab fix(deps): update rust crate rlua to 0.19.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-21 11:06:24 +00:00
5fae1fc403 fix(deps): update rust crate openssl to 0.10.63
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-01-20 00:10:57 +00:00
b9f7ff0a6f fix(deps): update rust crate clap to 4.4.18
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-16 20:43:06 +00:00
440245c332 fix(deps): update rust crate clap to 4.4.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-15 17:10:52 +00:00
a89af69c15 fix(deps): update rust crate clap to 4.4.16
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-12 03:16:36 +00:00
b93f96053e fix(deps): update rust crate clap to 4.4.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-11 17:07:16 +00:00
347d6171a5 fix(deps): update rust crate dagger-sdk to 0.3.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-09 21:06:32 +00:00
e7d90ffcc5 fix(deps): update rust crate libz-sys to 1.1.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-09 08:43:27 +00:00
9c607e8ab1 fix(deps): update rust crate clap to 4.4.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-08 17:04:44 +00:00
cf6a4b9fcd fix(deps): update rust crate libz-sys to 1.1.13
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-08 09:38:31 +00:00
a05699d24e fix(deps): update rust crate serde to 1.0.195
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-06 02:56:11 +00:00
ae9fdf7e7e fix(deps): update rust crate clap to 4.4.13
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-04 19:32:03 +00:00
8c3e546b27 fix(deps): update rust crate serde_json to 1.0.111
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-04 07:57:50 +00:00
248f294000 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-02 07:39:44 +00:00
65f4271957 fix(deps): update rust crate anyhow to 1.0.79
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-02 04:56:02 +00:00
eb980308c6 fix(deps): update rust crate serde_json to 1.0.109
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-01 01:31:19 +00:00
51e5e1a4ce fix(deps): update rust crate anyhow to 1.0.78
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-30 23:16:27 +00:00
00e45053f7 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-28 18:23:54 +00:00
dace148e52 fix(deps): update rust crate anyhow to 1.0.77
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-26 23:43:19 +00:00
04774a69bf fix(deps): update rust crate openssl to 0.10.62
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-22 13:22:05 +00:00
c489660211 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-21 20:59:41 +00:00
2e6e6fd328 fix(deps): update rust crate tokio to 1.35.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-19 18:19:37 +00:00
84df1fe4df fix(deps): update rust crate eyre to 0.6.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-13 22:11:11 +00:00
30c58b9eb2 fix(deps): update rust crate tokio to 1.35.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-08 23:21:48 +00:00
c2502dbf00 fix(deps): update rust crate eyre to 0.6.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-08 07:01:40 +00:00
bec95c9519 fix(deps): update rust crate openssl to 0.10.61
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-05 01:44:10 +00:00
02b1a627f0 fix(deps): update rust crate clap to 4.4.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-04 18:38:49 +00:00
4b84f27d67 fix(deps): update rust crate clap to 4.4.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-28 03:58:14 +00:00
a3bb366d07 fix(deps): update rust crate clap to 4.4.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-27 18:20:10 +00:00
c479cadf4d fix(deps): update rust crate openssl to 0.10.60
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-23 02:37:06 +00:00
74bd83aaec fix(deps): update rust crate serde to 1.0.193
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-21 02:27:47 +00:00
a707d31277 fix(deps): update rust crate eyre to 0.6.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-17 17:38:40 +00:00
8cfdebdaee fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-13 17:38:32 +00:00
f721e45f8f fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-09 20:10:17 +00:00
db4b41c032 fix(deps): update rust crate openssl to 0.10.59
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-03 14:50:30 +00:00
4945ecca40 fix(deps): update rust crate openssl to 0.10.58
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-01 21:34:10 +00:00
a6560a10cb fix(deps): update rust crate serde_json to 1.0.108
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-30 16:02:21 +00:00
e2feef1c27 feat: with multiple args
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-29 00:13:45 +02:00
1b9c9188d4 fix(deps): update rust crate tempfile to 3.8.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 22:57:15 +00:00
a9ca8cdc18 fix(deps): update rust crate serde_yaml to 0.9.27
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 05:49:14 +00:00
0431dd03bf fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 03:51:38 +00:00
e5eabf6901 fix(deps): update rust crate clap to 4.4.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-24 18:55:42 +00:00
a336025a0c fix(deps): update rust crate dagger-sdk to 0.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-21 09:17:54 +00:00
e471452ed3 fix(deps): update rust crate tracing to 0.1.40
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-19 01:37:43 +00:00
05ea0d8ed8 fix(deps): update rust crate tracing to 0.1.39
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-13 23:10:35 +00:00
c42ad69eef fix(deps): update rust crate serde to 1.0.189
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-13 03:08:00 +00:00
614e2bf442 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-09 10:09:53 +00:00
23d9ca8d11 fix(deps): update rust crate clap to 4.4.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-25 21:06:22 +00:00
05d8209f15 fix(deps): update rust crate git2 to 0.18.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-20 21:28:20 +00:00
8de0a15922 fix(deps): update rust crate clap to 4.4.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-18 16:04:16 +00:00
e20fbc56b4 fix(deps): update rust crate serde_json to 1.0.107
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-13 23:24:03 +00:00
aa1f6c3a16 fix(deps): update rust crate clap to 4.4.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-12 18:48:48 +00:00
e944203e80 fix(deps): update rust crate serde_json to 1.0.106
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-09 19:48:15 +00:00
3d4e6b6fcd fix(deps): update rust crate dagger-sdk to 0.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-06 21:26:33 +00:00
35d54ab30f fix(deps): update rust crate walkdir to 2.4.0
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build was killed
2023-09-05 14:40:02 +00:00
941a8f600f fix(deps): update rust crate tera to 1.19.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-03 16:31:01 +00:00
32d5fb6f97 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-31 18:58:51 +00:00
f97fe5c832 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-28 17:27:13 +00:00
8580813bbc fix(deps): update rust crate serde to 1.0.188
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-26 02:47:25 +00:00
65a995c9ee fix(deps): update rust crate serde to 1.0.187
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-25 18:00:45 +00:00
31c77c7581 fix(deps): update rust crate clap to 4.4.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-24 16:36:52 +00:00
dff0e85f7d fix(deps): update rust crate serde to 1.0.186
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-24 00:52:24 +00:00
ff74c55829 fix(deps): update rust crate clap to 4.3.24
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-23 14:54:02 +00:00
e6cd1c16b6 fix(deps): update rust crate serde to 1.0.185
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-21 04:48:27 +00:00
6b7f915c12 fix(deps): update rust crate tempfile to 3.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-18 23:00:57 +00:00
c8e4f4b66d fix(deps): update rust crate clap to 4.3.23
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-18 21:56:41 +00:00
0aa7195415 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-17 14:35:33 +00:00
1da2ded83e fix(deps): update rust crate dagger-sdk to 0.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-16 19:38:31 +00:00
60b234c0d7 fix(deps): update rust crate serde_json to 1.0.105
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-15 22:24:33 +00:00
cace45ee5b fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-15 16:10:40 +00:00
bc2999fb92 chore: add force to install
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:43:21 +02:00
26ef1cb0cd chore: full rename
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:42:13 +02:00
2cd9509fcb feat: rename cuddle_cli -> cuddle
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:41:07 +02:00
53b7513ceb fix(deps): update rust crate log to 0.4.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-12 11:30:03 +00:00
71965cb07c fix(deps): update rust crate tokio to 1.30.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-09 16:01:56 +00:00
dd94ee2e8e fix(deps): update rust crate clap to 4.3.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-08 02:22:57 +00:00
3bf7e837e5 fix(deps): update rust crate clap to 4.3.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-08 01:34:38 +00:00
39505938a5 fix(deps): update rust crate serde to 1.0.183
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-07 04:18:13 +00:00
724a364984 fix(deps): update rust crate tempfile to 3.7.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-06 22:09:19 +00:00
840694967d fix(deps): update rust crate openssl to 0.10.56
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-06 13:44:49 +00:00
b32643ff7a fix(deps): update rust crate serde to 1.0.182
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-06 05:49:51 +00:00
41d337d003 fix(deps): update rust crate serde to 1.0.181
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-04 00:51:28 +00:00
fd01de7ede feat: with rlua searcher
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-02 12:49:25 +02:00
44e8fe8918 chore: set docs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-02 12:35:53 +02:00
b7747dec06 feat: add license and publisable
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-02 12:35:36 +02:00
5e9dc88a1b docs: add readme
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-02 12:14:21 +02:00
ec1fdf267f feat: withname and path as proper variables
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-02 11:40:17 +02:00
c11baae1ba fix(deps): update rust crate serde to 1.0.180
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-07-31 18:52:07 +00:00
f396a36b37 fix(deps): update rust crate serde to 1.0.179
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-07-31 00:50:21 +00:00
8f155373f3 fix(deps): update rust crate serde to 1.0.178
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-07-28 23:52:57 +00:00
27b4e73a48 fix(deps): update rust crate serde to 1.0.177
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-07-27 18:33:39 +00:00
b58597e25d fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-07-27 14:07:01 +00:00
ae74427ca8 feat: remove cat
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:57:13 +02:00
4bd4648a94 feat: clean up drone
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:53:45 +02:00
26c1c083de feat: with extra context
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:50:58 +02:00
3a9f00b1e5 feat: with key checking wip
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:41:17 +02:00
02ec96e93c feat: revert to ed25519
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:37:31 +02:00
3fae1b2b06 feat: trying with rsa key instead
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:35:00 +02:00
c2cfee11b2 feat: with capital R
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:26:56 +02:00
c7fdd4fe17 feat: with updated permissions
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:26:26 +02:00
7e008bea09 feat: with inline eval
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:25:39 +02:00
83a8b5729e feat: with ssg agent
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:24:53 +02:00
a17c5d1421 feat: update with ssh agent support
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:24:04 +02:00
68b46e4bec feat: with updated docker file and friends
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 15:04:46 +02:00
33e1ff0e37 feat: with 1password
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 03:10:17 +02:00
32f7ce2459 feat: cuddle_cli -> cuddle
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 01:33:57 +02:00
e6d5f8c4b8 feat: with lua
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-02 12:21:12 +02:00
506a8e4a4e feat: with arg descriptions
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-02 01:00:04 +02:00
a483e28a70 feat: with description
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-02 00:54:03 +02:00
12bd424f18 feat: with schema
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-02 00:47:14 +02:00
6a82e0c10a feat: build with ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-25 20:40:12 +02:00
f8035357b7 feat: with direct output
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-25 13:43:42 +02:00
dc0fa589a5 feat: with redone output
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-17 13:33:10 +02:00
04e8baeefc feat: with variables
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-17 13:05:17 +02:00
2241941f0e feat: add templating to project
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-17 12:54:06 +02:00
2f2fdb9631 chore: cargo fix
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-17 11:08:52 +02:00
6c5fed87b1 feat: with init command
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-17 03:38:48 +02:00
91ee9d4387 feat: update cli with bin name
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-09 22:03:32 +02:00
77bbf6c855 Merge pull request 'Configure Renovate' (#2) from renovate/configure into main
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone Build was killed
Reviewed-on: https://git.front.kjuulh.io/kjuulh/cuddle/pulls/2
2022-10-25 20:43:50 +00:00
5b0eecd83d Add renovate.json
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2022-10-25 20:35:20 +00:00
04f3bc1295 revert naming
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-12 23:47:32 +02:00
1727836c1f with new base url WIP should come from environment instead
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-12 23:10:32 +02:00
aae8201252 Remove drone
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-18 21:17:29 +02:00
4e5f451bf6 with newer dind
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-21 16:59:40 +02:00
581404c622 fix stuff
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
2022-08-14 22:10:58 +02:00
0ec196e8c9 with real exit
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-14 22:06:31 +02:00
4313c60056 with code
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-14 21:34:33 +02:00
21cda03b6d name change
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-14 21:11:31 +02:00
f8c5ae93b1 drone with cuddle
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-14 21:10:03 +02:00
21b779a03d with local cuddle
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-14 21:08:18 +02:00
4765502109 Added cuddle
All checks were successful
continuous-integration/drone Build is passing
2022-08-14 21:06:40 +02:00
a1472194d2 fixed cuddle image 2022-08-14 21:06:07 +02:00
54 changed files with 1858 additions and 1991 deletions

2
.drone.yml Normal file
View File

@@ -0,0 +1,2 @@
kind: template
load: cuddle-rust-cli-plan.yaml

2
.gitignore vendored
View File

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

1402
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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"}
```

View 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"

View 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)
}
}

View 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
View File

@@ -0,0 +1 @@
/target

19
crates/cuddle/Cargo.toml Normal file
View 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"

View File

@@ -0,0 +1 @@
[plan]

View File

@@ -0,0 +1,5 @@
[plan]
path = "../plan"
[project]
name = "basic"

View File

@@ -0,0 +1,5 @@
[plan]
git = "ssh://git@git.front.kjuulh.io/kjuulh/cuddle-git-example.git"
[project]
name = "git"

View File

@@ -0,0 +1 @@
{"ProjectSchema":{"fields":{"name":{"fields":null,"type":null,"contracts":["String"],"documentation":null}},"type":null,"contracts":[],"documentation":null}}

View File

@@ -0,0 +1,2 @@
[plan]
schema = { nickel = "schema.ncl" }

View File

@@ -0,0 +1,5 @@
{
ProjectSchema = {
name | String,..
}
}

View File

@@ -0,0 +1,5 @@
[project]
name = "schema"
[plan]
path = "../plan"

88
crates/cuddle/src/cli.rs Normal file
View 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)
}
}

View 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(())
}
}

View 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
View 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
View 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(())
}
}

View 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(())
}
}

View 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,
)?)),
}
}
}

View 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(())
}
}

View 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 {}

View 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())
}
}
}
}

15
cuddle.yaml Normal file
View File

@@ -0,0 +1,15 @@
# 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-rust-cli-plan.git"
vars:
service: "cuddle"
registry: kasperhermansen
please:
project:
owner: kjuulh
repository: "cuddle"
branch: "main"
settings:
api_url: "https://git.front.kjuulh.io"

View File

@@ -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"

View File

@@ -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!")),
}
}
}

View File

@@ -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(())
}
}

View File

@@ -1,182 +0,0 @@
mod subcommands;
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use clap::Command;
use crate::{
actions::CuddleAction,
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>,
}
impl<'a> CuddleCli<'a> {
pub fn new(context: Arc<Mutex<Vec<CuddleContext>>>) -> anyhow::Result<CuddleCli<'a>> {
let mut cli = CuddleCli {
scripts: vec![],
variables: vec![],
context: context.clone(),
command: None,
tmp_dir: None,
};
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"))?;
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)
}
}

View File

@@ -1,2 +0,0 @@
pub mod render_template;
pub mod x;

View File

@@ -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)),
}
}

View File

@@ -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")),
}
}

View File

@@ -1,28 +0,0 @@
use envconfig::Envconfig;
pub enum CuddleFetchPolicy {
Always,
Once,
Never,
}
#[derive(Envconfig, Clone)]
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")),
}
}
}

View File

@@ -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"
))
}

View File

@@ -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)?.execute()?;
Ok(())
}
fn init_logging() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.pretty()
.with_max_level(Level::INFO)
.init();
Ok(())
}

View File

@@ -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 }
}
}

View File

@@ -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(),
})
}
}

View File

@@ -1 +0,0 @@
pub mod git;

View File

@@ -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]

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
#!/bin/bash
echo "Ran build"

View File

@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
some {{ service }} name
- {{ extravar }}

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -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"
}

View 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'