Compare commits

163 Commits

Author SHA1 Message Date
4017adf763 fix(deps): update all dependencies
Some checks failed
renovate/artifacts Artifact file update failure
2025-07-26 03:20:30 +00:00
15298f1f96 feat: update 2025-05-25 16:14:36 +02:00
b86fec2a58 fix(deps): update rust crate serde to v1.0.218
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-21 01:30:01 +00:00
687792706b fix(deps): update rust crate serde to v1.0.217
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-28 01:27:22 +00:00
1c29770ce2 fix(deps): update rust crate serde to v1.0.216
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-11 05:23:24 +00:00
c1e1215f3c fix(deps): update rust crate tracing-subscriber to v0.3.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-30 01:40:42 +00:00
e94513c24f fix(deps): update rust crate tracing to v0.1.41
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-28 01:26:04 +00:00
6aa90d22ea feat: fix additional properties
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 16:58:24 +01:00
8fe00b22c5 fix(deps): update rust crate serde to v1.0.215
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-12 01:21:03 +00:00
78262a138d fix(deps): update rust crate serde to v1.0.214
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-29 01:25:57 +00:00
bcebe4bce4 fix(deps): update rust crate serde to v1.0.213
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-23 00:26:20 +00:00
1f8a5d52c4 fix(deps): update rust crate futures-util to v0.3.31
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-06 00:22:52 +00:00
9207ef8f81 fix(deps): update rust crate serde to v1.0.210
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-07 00:20:13 +00:00
7df629290a fix(deps): update rust crate serde to v1.0.209
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-24 03:46:41 +00:00
54829a7fe4 chore(test): test commit
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-29 19:44:39 +02:00
0ebe88a470 feat: allow arrays
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 22:59:11 +02:00
67af5e7aa6 fix(deps): update rust crate serde to v1.0.203
Some checks reported errors
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build encountered an error
2024-05-25 18:35:36 +00: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
40 changed files with 4359 additions and 894 deletions

View File

@@ -13,12 +13,10 @@ steps:
from_secret: gitea_id_ed25519 from_secret: gitea_id_ed25519
commands: commands:
- mkdir -p $HOME/.ssh/ - mkdir -p $HOME/.ssh/
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519 - echo "$SSH_KEY" | base64 -d > $HOME/.ssh/id_ed25519
- ls $HOME/.ssh/
- cat $HOME/.ssh/id_ed25519
- name: build - name: build
image: kasperhermansen/cuddle-image:latest image: kasperhermansen/cuddle:latest
pull: always pull: always
volumes: volumes:
- name: ssh - name: ssh
@@ -26,27 +24,21 @@ steps:
- name: dockersock - name: dockersock
path: /var/run path: /var/run
commands: commands:
- apk add bash - eval `ssh-agent`
- cuddle_cli x build_cuddle_image - chmod -R 600 ~/.ssh
- ssh-add
- cuddle x build_cuddle_image
environment: environment:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
DOCKER_USERNAME: CUDDLE_SECRETS_PROVIDER: 1password
from_secret: docker_username CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
DOCKER_PASSWORD: CUDDLE_SSH_AGENT: "true"
from_secret: docker_password OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
depends_on: depends_on:
- "load_secret" - "load_secret"
- name: send telegram notification
image: appleboy/drone-telegram
settings:
token:
from_secret: telegram_token
to: 2129601481
format: markdown
when:
status: [failure]
services: services:
- name: docker - name: docker
image: docker:dind image: docker:dind

2
.env.ci Normal file
View File

@@ -0,0 +1,2 @@
DOCKER_USERNAME={{ op://application/docker_hub_credentials/username }}
DOCKER_PASSWORD={{ op://application/docker_hub_credentials/password }}

1
.gitignore vendored
View File

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

2586
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2023 Kasper J. Hermansen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

71
README.md Normal file
View File

@@ -0,0 +1,71 @@
# Cuddle - Configuration and Script Manager
Cuddle CLI is a Rust command-line interface application designed to manage
configuration variables and scripts across projects. It simplifies sharing of
code and workflows, making development and collaboration smoother and more
efficient. This project is published on crates.io as `cuddle`.
## Table of Contents
1. [Installation](#installation)
2. [Usage](#usage)
3. [Commands](#commands)
4. [Configuration](#configuration)
5. [Contributing](#contributing)
6. [License](#license)
## Installation
Make sure you have Rust and Cargo installed. You can install Rust and Cargo from
[https://rustup.rs/](https://rustup.rs/).
To install Cuddle CLI, run:
```sh
cargo install cuddle
```
## Usage
After successful installation, you can run the CLI using the `cuddle` command:
```sh
cuddle [command] [options]
```
## Commands
Detailed documentation of the commands and options can be found in our
[official documentation (tbd)](LINK_TO_DOCUMENTATION).
## Configuration
You can manage your configurations in a file called `cuddle.yaml`. For details
on how to format this file, check our
[Configuration Guide (tbd)](LINK_TO_CONFIGURATION_GUIDE).
## Contributing
Contributions are what make the open-source community such an amazing place to
learn, inspire, and create. Any contributions you make are **greatly
appreciated**.
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
Please read our [Contributing Guide](LINK_TO_CONTRIBUTING_GUIDE) for more
information.
## License
Distributed under the MIT License. See `LICENSE` for more information.
## Contact
You can reach out to us at our
[official contact page (tbd)](LINK_TO_CONTACT_PAGE). Please ensure to follow our
[Code of Conduct (tbd)](LINK_TO_CONDUCT_PAGE) when interacting with our
community.

11
ci/Cargo.toml Normal file
View File

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

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

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

View File

@@ -3,10 +3,12 @@
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git" base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars: vars:
service: "cuddle-image" service: "cuddle"
registry: kasperhermansen registry: kasperhermansen
scripts: scripts:
install:
type: shell
build_cuddle_image: build_cuddle_image:
type: shell type: shell
args: args:

51
cuddle/Cargo.toml Normal file
View File

@@ -0,0 +1,51 @@
[package]
name = "cuddle"
description = "cuddle is a shuttle inspired script and configuration management tool. It enables sharing of workflows on developers workstations and ci"
repository = "https://git.front.kjuulh.io/kjuulh/cuddle"
readme = "../README.md"
license-file = "../LICENSE"
publish = true
version = "0.2.0"
edition = "2021"
[[bin]]
name = "cuddle"
path = "src/main.rs"
[dependencies]
anyhow = { version = "1.0.79", features = ["backtrace"] }
serde = { version = "1.0.196", features = ["derive"] }
serde_yaml = "0.9.31"
walkdir = "2.4.0"
git2 = { version = "0.20.0", default-features = false, features = [
"vendored-libgit2",
"vendored-openssl",
"ssh",
] }
clap = { version = "4.4.18", features = ["env", "string"] }
envconfig = "0.11.0"
dirs = "6.0.0"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
log = { version = "0.4.20", features = ["std", "kv_unstable"] }
tera = "1.19.1"
openssl = { version = "0.10.63", features = ["vendored"] }
libz-sys = { version = "1.1.15", default-features = false, features = [
"libc",
"static",
] }
inquire = { version = "0.7.0", features = ["console"] }
tempfile = { version = "3.10.0" }
serde_json = "1.0.113"
rlua = "0.20.0"
rlua-searcher = "0.1.0"
dotenvy = { version = "0.15.7" }
blake3 = "1.5.0"
tokio = { version = "1.36.0", features = ["full"] }
futures-util = "0.3.30"
fs_extra = "1.3.0"
[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["rustls-tls", "json"]

366
cuddle/src/actions/mod.rs Normal file
View File

@@ -0,0 +1,366 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::Context;
use clap::ArgMatches;
use rlua::Lua;
use rlua_searcher::AddSearcher;
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 description: Option<String>,
pub name: String,
}
#[allow(dead_code)]
impl CuddleAction {
pub fn new(
script: CuddleScript,
path: PathBuf,
name: String,
description: Option<String>,
) -> Self {
Self {
script,
path,
name,
description,
}
}
pub fn execute(
self,
matches: &ArgMatches,
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 = matches.get_one::<String>(&k).cloned().ok_or(
anyhow::anyhow!(
"failed to find env variable with key: {}",
&e.key
),
)?;
CuddleVariable::new(k.clone(), env_var)
}
CuddleShellScriptArg::Flag(flag) => {
match matches.get_one::<String>(&flag.name) {
Some(flag_var) => {
CuddleVariable::new(k.clone(), flag_var.clone())
}
None => continue,
}
}
};
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!")),
CuddleScript::Lua(l) => {
let lua = Lua::new();
let mut map = HashMap::new();
//map.insert("init".into(), "print(\"something\")".into());
let lua_dir = PathBuf::new().join(&self.path).join("scripts").join("lua");
if lua_dir.exists() {
let absolute_lua_dir = lua_dir.canonicalize()?;
for entry in walkdir::WalkDir::new(&lua_dir)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.metadata()?.is_file() {
let full_file_path = entry.path().canonicalize()?;
let relative_module_path =
full_file_path.strip_prefix(&absolute_lua_dir)?;
let module_path = relative_module_path
.to_string_lossy()
.to_string()
.trim_end_matches("/init.lua")
.trim_end_matches(".lua")
.replace("/", ".");
let contents = std::fs::read_to_string(entry.path())?;
tracing::trace!(module_path = &module_path, "adding lua file");
map.insert(module_path.into(), contents.into());
}
}
}
let lua_rocks_dir = PathBuf::new()
.join(&self.path)
.join("lua_modules")
.join("share")
.join("lua")
.join("5.4");
if lua_rocks_dir.exists() {
let absolute_lua_dir = lua_rocks_dir.canonicalize()?;
for entry in walkdir::WalkDir::new(&lua_rocks_dir)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.metadata()?.is_file() {
let full_file_path = entry.path().canonicalize()?;
let relative_module_path =
full_file_path.strip_prefix(&absolute_lua_dir)?;
let module_path = relative_module_path
.to_string_lossy()
.to_string()
.trim_end_matches("/init.lua")
.trim_end_matches(".lua")
.replace("/", ".");
let contents = std::fs::read_to_string(entry.path())?;
tracing::trace!(module_path = &module_path, "adding lua file");
map.insert(module_path.into(), contents.into());
}
}
}
lua.context::<_, anyhow::Result<()>>(|lua_ctx| {
lua_ctx.add_searcher(map)?;
let globals = lua_ctx.globals();
let lua_script_entry = std::fs::read_to_string(
PathBuf::new()
.join(&self.path)
.join("scripts")
.join(format!("{}.lua", &self.name)),
)
.context("failed to find lua script")?;
lua_ctx
.load(&lua_script_entry)
.set_name(&self.name)?
.exec()?;
Ok(())
})?;
Ok(())
}
CuddleScript::Rust(script) => Ok(()),
}
}
}
pub mod rust_action {
use std::{path::PathBuf, time::Duration};
use anyhow::Context;
use futures_util::StreamExt;
use reqwest::Method;
use tokio::{fs::File, io::AsyncWriteExt};
use crate::model::{CuddleRustScript, CuddleRustUpstream, CuddleVariable};
pub struct RustActionConfig {
pub config_dir: PathBuf,
pub cache_dir: PathBuf,
}
impl Default for RustActionConfig {
fn default() -> Self {
let config = dirs::config_dir().expect("to be able to find a valid .config dir");
let cache = dirs::cache_dir().expect("to be able to find a valid .cache dir");
Self {
config_dir: config,
cache_dir: cache,
}
}
}
pub struct RustAction {
pub config: RustActionConfig,
pub plan: String,
pub binary_name: String,
}
impl RustAction {
pub fn new(plan: String, binary_name: String) -> Self {
Self {
plan,
binary_name,
config: RustActionConfig::default(),
}
}
pub async fn execute(
&self,
script: CuddleRustScript,
variables: impl IntoIterator<Item = CuddleVariable>,
) -> anyhow::Result<()> {
let commit_sha = self
.get_commit_sha()
.await
.context("failed to find a valid commit sha on the inferred path: .cuddle/plan")?;
let binary_hash = self.calculate_hash(commit_sha)?;
// Get cached binary
// let binary = match self.get_binary(&binary_hash).await? {
// Some(binary) => binary,
// None => self.fetch_binary(&script, &binary_hash).await?,
// };
// Execute binary
Ok(())
}
async fn get_binary(
&self,
binary_hash: impl Into<String>,
) -> anyhow::Result<Option<RustBinary>> {
let binary_path = self.get_cached_binary_path(binary_hash);
if !binary_path.exists() {
return Ok(None);
}
Ok(Some(RustBinary {}))
}
fn get_cached_binary_path(&self, binary_hash: impl Into<String>) -> PathBuf {
let cached_binary_name = self.get_cached_binary_name(binary_hash);
let binary_path = self
.config
.cache_dir
.join("binaries")
.join(cached_binary_name);
binary_path
}
#[inline]
fn get_cached_binary_name(&self, binary_hash: impl Into<String>) -> String {
format!("{}-{}", binary_hash.into(), self.binary_name)
}
async fn get_commit_sha(&self) -> anyhow::Result<String> {
let repo = git2::Repository::open(".cuddle/plan")?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
let commit_sha = commit.id();
Ok(commit_sha.to_string())
}
// async fn fetch_binary(
// &self,
// script: &CuddleRustScript,
// binary_hash: impl Into<String>,
// ) -> anyhow::Result<RustBinary> {
//let upstream = &script.upstream;
//TODO: we should interpret some template variables in the upstream string. Ignore for now though
// match UpstreamRustBinary::from(upstream) {
// UpstreamRustBinary::HttpBased { url } => {
// let client = reqwest::ClientBuilder::new()
// .user_agent(concat!(
// env!("CARGO_PKG_NAME"),
// "/",
// env!("CARGO_PKG_VERSION")
// ))
// .connect_timeout(Duration::from_secs(5))
// .build()?;
// let resp = client.request(Method::GET, url).send().await?;
// let mut stream = resp.bytes_stream();
// let mut file = File::create(self.get_cached_binary_name(binary_hash)).await?;
// while let Some(item) = stream.next().await {
// let chunk = item?;
// file.write_all(&chunk).await?;
// }
// // Make sure the entire file is written before we execute it
// file.flush().await?;
// todo!()
// }
// }
// }
fn calculate_hash(&self, commit_sha: impl Into<Vec<u8>>) -> anyhow::Result<String> {
let mut contents: Vec<u8> = Vec::new();
contents.append(&mut self.plan.clone().into_bytes());
contents.append(&mut commit_sha.into());
let hash = blake3::hash(&contents);
let hex = hash.to_hex();
Ok(hex.to_string())
}
}
pub struct RustBinary {}
pub enum UpstreamRustBinary {
HttpBased { url: String },
}
impl From<CuddleRustUpstream> for UpstreamRustBinary {
fn from(value: CuddleRustUpstream) -> Self {
match value {
CuddleRustUpstream::Gitea { url } => Self::HttpBased { url },
}
}
}
impl From<&CuddleRustUpstream> for UpstreamRustBinary {
fn from(value: &CuddleRustUpstream) -> Self {
match value {
CuddleRustUpstream::Gitea { url } => Self::HttpBased { url: url.clone() },
}
}
}
}

View File

@@ -1,3 +1,5 @@
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use std::{env::current_dir, path::PathBuf, process::Command}; use std::{env::current_dir, path::PathBuf, process::Command};
use crate::model::CuddleVariable; use crate::model::CuddleVariable;
@@ -16,15 +18,7 @@ impl ShellAction {
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> { pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
log::debug!("executing shell action: {}", self.path.clone()); log::debug!("executing shell action: {}", self.path.clone());
log::info!( log::info!("Starting running shell action: {}", self.path.clone());
"
===
Starting running shell action: {}
===
",
self.path.clone()
);
let path = PathBuf::from(self.path.clone()); let path = PathBuf::from(self.path.clone());
if !path.exists() { if !path.exists() {
@@ -35,25 +29,33 @@ Starting running shell action: {}
let current_dir = current_dir()?; let current_dir = current_dir()?;
log::trace!("current executable dir={}", current_dir.to_string_lossy()); log::trace!("current executable dir={}", current_dir.to_string_lossy());
let mut process = Command::new(path) let mut process = Command::new(&path)
.current_dir(current_dir) .current_dir(current_dir)
.envs(variables.iter().map(|v| { .envs(variables.iter().rev().map(|v| {
log::trace!("{:?}", v); log::trace!("{:?}", v);
(v.name.to_uppercase(), v.value.clone()) (v.name.to_uppercase(), v.value.clone())
})) }))
.spawn()?; .spawn()?;
process.wait()?; let status = process.wait()?;
log::info!( match status.code() {
" None => {
log::warn!("process exited without code")
}
Some(n) => {
if n > 0 {
return Err(anyhow::anyhow!(
"{} exited with: {}",
path.clone().to_string_lossy(),
n
));
}
}
}
=== log::info!("Finished running shell action");
Finished running shell action
===
"
);
Ok(()) Ok(())
} }

413
cuddle/src/cli/mod.rs Normal file
View File

@@ -0,0 +1,413 @@
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::RenderCommand, render_kustomize::RenderKustomizeCommand,
render_template::RenderTemplateCommand,
};
#[derive(Debug, Clone)]
pub struct CuddleCli {
scripts: Vec<CuddleAction>,
variables: Vec<CuddleVariable>,
context: Option<Arc<Mutex<Vec<CuddleContext>>>>,
command: Option<Command>,
tmp_dir: Option<PathBuf>,
config: CuddleConfig,
}
impl CuddleCli {
pub fn new(
context: Option<Arc<Mutex<Vec<CuddleContext>>>>,
config: CuddleConfig,
) -> anyhow::Result<CuddleCli> {
let mut cli = CuddleCli {
scripts: vec![],
variables: vec![],
context: context.clone(),
command: None,
tmp_dir: None,
config,
};
if let Ok(provider) = std::env::var("CUDDLE_SECRETS_PROVIDER") {
let provider = provider
.split(",")
.map(|p| p.to_string())
.collect::<Vec<_>>();
tracing::trace!("secrets-provider enabled, handling for each entry");
handle_providers(provider)?;
std::thread::sleep(std::time::Duration::from_millis(100));
}
match context {
Some(_) => {
tracing::debug!("build full cli");
cli = cli
.process_variables()
.process_scripts()
.process_templates()?
.build_cli();
}
None => {
tracing::debug!("build bare cli");
cli = cli.build_bare_cli();
}
}
Ok(cli)
}
fn process_variables(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().unwrap().lock() {
for ctx in context_iter.iter() {
if let Some(variables) = ctx.plan.vars.clone() {
let mut variables: Vec<CuddleVariable> = variables.into();
self.variables.append(&mut variables);
}
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",
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", commit.commit_sha.clone())),
Err(e) => {
log::debug!("{}", e);
}
}
self
}
fn process_scripts(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().unwrap().lock() {
for ctx in context_iter.iter() {
if let Some(scripts) = ctx.plan.scripts.clone() {
for (name, script) in scripts {
match &script {
CuddleScript::Shell(shell_script) => {
self.scripts.push(CuddleAction::new(
script.clone(),
ctx.path.clone(),
name,
shell_script.description.clone(),
))
}
CuddleScript::Dagger(_) => todo!(),
CuddleScript::Lua(l) => self.scripts.push(CuddleAction::new(
script.clone(),
ctx.path.clone(),
name,
l.description.clone(),
)),
CuddleScript::Rust(_) => todo!(),
}
}
}
}
}
self
}
fn process_templates(self) -> anyhow::Result<Self> {
if self.tmp_dir.is_none() {
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().unwrap().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)? {
let f = file?;
let mut dest_file = tmp_dir.clone();
dest_file.push(f.path().strip_prefix(&template_path)?.parent().unwrap());
tracing::trace!(
"moving from: {} to {}",
f.path().display(),
dest_file.display()
);
if f.path().is_dir() {
std::fs::create_dir_all(&dest_file)?;
}
fs_extra::copy_items(
&[f.path()],
&dest_file,
&fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
..Default::default()
},
)?;
}
}
}
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)
.arg(clap::Arg::new("secrets-provider").long("secrets-provider").env("CUDDLE_SECRETS_PROVIDER"));
root_cmd = subcommands::x::build_command(root_cmd, self.clone());
root_cmd = subcommands::render_template::build_command(root_cmd);
root_cmd = subcommands::render_kustomize::build_command(root_cmd);
root_cmd = subcommands::render::build_command(root_cmd);
root_cmd = subcommands::init::build_command(root_cmd, self.clone());
self.command = Some(root_cmd);
self
}
fn build_bare_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::init::build_command(root_cmd, self.clone());
self.command = Some(root_cmd);
self
}
pub fn execute(self) -> anyhow::Result<Self> {
if let Some(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", sub_matches)) => {
RenderCommand::execute(sub_matches, self.clone())?;
Ok(())
}
Some(("render_template", sub_matches)) => {
RenderTemplateCommand::from_matches(sub_matches, self.clone())
.and_then(|cmd| cmd.execute())?;
Ok(())
}
Some(("render-kustomize", sub_matches)) => {
RenderKustomizeCommand::from_matches(sub_matches, self.clone())
.and_then(|cmd| cmd.execute())?;
Ok(())
}
Some(("init", sub_matches)) => {
subcommands::init::execute_init(sub_matches, self.clone())
}
_ => Err(anyhow::anyhow!("could not find a match")),
};
match res {
Ok(()) => {}
Err(e) => {
return Err(e);
}
}
}
Ok(self)
}
}
pub enum SecretProvider {
OnePassword {
inject: Vec<String>,
dotenv: Option<String>,
},
}
impl TryFrom<String> for SecretProvider {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.as_str() {
"1password" => {
let one_password_inject = std::env::var("CUDDLE_ONE_PASSWORD_INJECT")
.ok()
.filter(|f| f.as_str() != "");
let one_password_dot_env = std::env::var("CUDDLE_ONE_PASSWORD_DOT_ENV").ok();
let injectables = one_password_inject
.unwrap_or(String::new())
.split(",")
.filter(|s| s.contains('='))
.map(|i| i.to_string())
.collect::<Vec<_>>();
// for i in &injectables {
// if !std::path::PathBuf::from(i).exists() {
// anyhow::bail!("1pass injectable path doesn't exist: {}", i);
// }
// }
if let Some(one_password_dot_env) = &one_password_dot_env {
if let Ok(dir) = std::env::current_dir() {
tracing::trace!(
current_dir = dir.display().to_string(),
dotenv = &one_password_dot_env,
exists = PathBuf::from(&one_password_dot_env).exists(),
"1password dotenv inject"
);
}
}
Ok(Self::OnePassword {
inject: injectables,
dotenv: if let Some(one_password_dot_env) = one_password_dot_env {
if PathBuf::from(&one_password_dot_env).exists() {
Some(one_password_dot_env)
} else {
None
}
} else {
None
},
})
}
value => {
tracing::debug!(
"provided secrets manager doesn't match any allowed values {}",
value
);
Err(anyhow::anyhow!("value is not one of supported values"))
}
}
}
}
fn handle_providers(provider: Vec<String>) -> anyhow::Result<()> {
fn execute_1password(lookup: &str) -> anyhow::Result<String> {
let out = std::process::Command::new("op")
.arg("read")
.arg(lookup)
.output()?;
let secret = std::str::from_utf8(&out.stdout)?;
Ok(secret.to_string())
}
fn execute_1password_inject(file: &str) -> anyhow::Result<Vec<(String, String)>> {
let out = std::process::Command::new("op")
.arg("inject")
.arg("--in-file")
.arg(file)
.output()?;
let secrets = std::str::from_utf8(&out.stdout)?.split('\n');
let secrets_pair = secrets
.map(|secrets_pair| secrets_pair.split_once("="))
.flatten()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<Vec<(String, String)>>();
Ok(secrets_pair)
}
let res = provider
.into_iter()
.map(|p| SecretProvider::try_from(p))
.collect::<anyhow::Result<Vec<_>>>();
let res = res?;
let res = res
.into_iter()
.map(|p| match p {
SecretProvider::OnePassword { inject, dotenv } => {
tracing::trace!(
inject = inject.join(","),
dotenv = dotenv,
"handling 1password"
);
if let Some(dotenv) = dotenv {
let pairs = execute_1password_inject(&dotenv).unwrap();
for (key, value) in pairs {
tracing::debug!(env_name = &key, value=&value, "set var from 1password");
std::env::set_var(key, value);
}
}
for i in inject {
let (env_var_name, op_lookup) = i.split_once("=").ok_or(anyhow::anyhow!(
"CUDDLE_ONE_PASSWORD_INJECT is not a key value pair ie. key:value,key2=value2"
))?;
let secret = execute_1password(&op_lookup)?;
std::env::set_var(&env_var_name, secret);
tracing::debug!(
env_name = &env_var_name,
lookup = &op_lookup,
"set var from 1password"
);
}
Ok(())
}
})
.collect::<anyhow::Result<Vec<()>>>();
let _ = res?;
Ok(())
}

View File

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

View File

@@ -0,0 +1,227 @@
use std::collections::BTreeMap;
use std::fs::{create_dir_all, read, read_dir};
use std::io::Write;
use std::path::PathBuf;
use anyhow::Context;
use clap::{ArgMatches, Command};
use walkdir::WalkDir;
use crate::cli::CuddleCli;
pub fn build_command(root_cmd: Command, _cli: CuddleCli) -> Command {
let mut repo_url = clap::Arg::new("repo").long("repo").short('r');
if let Ok(cuddle_template_url) = std::env::var("CUDDLE_TEMPLATE_URL") {
repo_url = repo_url.default_value(cuddle_template_url);
} else {
repo_url = repo_url.required(true);
}
let execute_cmd = Command::new("init")
.about("init bootstraps a repository from a template")
.arg(repo_url)
.arg(clap::Arg::new("name").long("name"))
.arg(clap::Arg::new("path").long("path"))
.arg(clap::Arg::new("value").short('v').long("value"));
root_cmd.subcommand(execute_cmd)
}
pub fn execute_init(exe_submatch: &ArgMatches, _cli: CuddleCli) -> anyhow::Result<()> {
let repo = exe_submatch.get_one::<String>("repo").unwrap();
let name = exe_submatch.get_one::<String>("name");
let path = exe_submatch.get_one::<String>("path");
let values = exe_submatch
.get_many::<String>("value")
.unwrap_or_default()
.collect::<Vec<_>>();
tracing::info!("Downloading: {}", repo);
create_dir_all(std::env::temp_dir())?;
let tmpdir = tempfile::tempdir()?;
let tmpdir_path = tmpdir.path().canonicalize()?;
let output = std::process::Command::new("git")
.args(&["clone", repo, "."])
.current_dir(tmpdir_path)
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
let templates_path = tmpdir.path().join("cuddle-templates.json");
let template_path = tmpdir.path().join("cuddle-template.json");
let templates = if templates_path.exists() {
let templates = read(templates_path)?;
let templates: CuddleTemplates = serde_json::from_slice(&templates)?;
let mut single_templates = Vec::new();
for template_name in templates.templates.iter() {
let template = read(
tmpdir
.path()
.join(template_name)
.join("cuddle-template.json"),
)?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
single_templates.push((template_name, template))
}
single_templates
.into_iter()
.map(|(name, template)| (name.clone(), tmpdir.path().join(name), template))
.collect::<Vec<_>>()
} else if template_path.exists() {
let template = read(template_path)?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
vec![(template.clone().name, tmpdir.path().to_path_buf(), template)]
} else {
anyhow::bail!("No cuddle-template.json or cuddle-templates.json found");
};
let template = match name {
Some(name) => {
let template = read(tmpdir.path().join(name).join("cuddle-template.json"))?;
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
Ok((name.clone(), tmpdir.path().join(name), template))
}
None => {
if templates.len() > 1 {
let name = inquire::Select::new(
"template",
templates.iter().map(|t| t.0.clone()).collect(),
)
.with_help_message("name of which template to use")
.prompt()?;
let found_template = templates
.iter()
.find(|item| item.0 == name)
.ok_or(anyhow::anyhow!("could not find an item with that name"))?;
Ok(found_template.clone())
} else if templates.len() == 1 {
Ok(templates[0].clone())
} else {
Err(anyhow::anyhow!("No templates found, with any valid names"))
}
}
};
let (_name, template_dir, mut template) = template?;
let path = match path {
Some(path) => path.clone(),
None => inquire::Text::new("path")
.with_help_message("to where it should be placed")
.with_default(".")
.prompt()?,
};
create_dir_all(&path)?;
let dir = std::fs::read_dir(&path)?;
if dir.count() != 0 {
for entry in read_dir(&path)? {
let entry = entry?;
if entry.file_name() == ".git" || entry.file_name() == ".jj" {
continue;
} else {
anyhow::bail!("Directory {} is not empty", &path);
}
}
}
{
if let Some(ref mut prompt) = template.prompt {
'prompt: for (name, prompt) in prompt {
for value in &values {
if let Some((value_name, value_content)) = value.split_once("=") {
if value_name == name {
prompt.value = value_content.to_string();
continue 'prompt;
}
}
}
let value = inquire::Text::new(&name)
.with_help_message(&prompt.description)
.prompt()?;
prompt.value = value;
}
}
}
for entry in WalkDir::new(&template_dir).follow_links(false) {
let entry = entry?;
let entry_path = entry.path();
let new_path = PathBuf::from(&path).join(entry_path.strip_prefix(&template_dir)?);
let new_path = replace_with_variables(&new_path.to_string_lossy().to_string(), &template)?;
let new_path = PathBuf::from(new_path);
if entry_path.is_dir() {
create_dir_all(&new_path)?;
}
if entry_path.is_file() {
let name = entry.file_name();
if let Some(parent) = entry_path.parent() {
create_dir_all(parent)?;
}
if name == "cuddle-template.json" || name == "cuddle-templates.json" {
continue;
}
tracing::info!("writing to: {}", new_path.display());
let old_content = match std::fs::read_to_string(entry_path) {
Ok(e) => e,
Err(e) => {
tracing::debug!("found invalid file possibly with invalid utf8: {}", e);
std::fs::copy(entry_path, new_path).context("failed to write file")?;
continue;
}
};
let new_content = replace_with_variables(&old_content, &template)?;
std::fs::write(new_path, new_content.as_bytes())?;
}
}
Ok(())
}
fn replace_with_variables(content: &str, template: &CuddleTemplate) -> anyhow::Result<String> {
let mut content = content.to_string();
if let Some(prompt) = &template.prompt {
for (name, value) in prompt {
content = content.replace(&format!("%%{}%%", name), &value.value);
}
}
Ok(content)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct CuddleTemplates {
pub templates: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct CuddleTemplate {
pub name: String,
pub prompt: Option<BTreeMap<String, CuddleTemplatePrompt>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct CuddleTemplatePrompt {
pub description: String,
#[serde(skip)]
pub value: String,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
use clap::{Arg, ArgMatches, Command};
use crate::cli::CuddleCli;
pub fn build_command(root_cmd: Command, cli: CuddleCli) -> Command {
if cli.scripts.len() > 0 {
let execute_cmd_about = "x is your entry into your domains scripts, scripts inherited from parents will also be present here";
let mut execute_cmd = Command::new("x")
.about(execute_cmd_about)
.subcommand_required(true);
execute_cmd = execute_cmd.subcommands(&build_scripts(cli));
root_cmd.subcommand(execute_cmd)
} else {
root_cmd
}
}
pub fn build_scripts(cli: CuddleCli) -> Vec<Command> {
let mut cmds = Vec::new();
for script in cli.scripts.iter() {
let mut cmd = Command::new(&script.name);
if let Some(desc) = &script.description {
cmd = cmd.about(desc)
}
match &script.script {
crate::model::CuddleScript::Shell(shell_script) => {
if let Some(args) = &shell_script.args {
for (arg_name, arg) in args {
cmd = match arg {
crate::model::CuddleShellScriptArg::Env(arg_env) => cmd.arg(
Arg::new(arg_name.clone())
.env(arg_name.to_uppercase().replace(".", "_"))
.required(true),
),
crate::model::CuddleShellScriptArg::Flag(arg_flag) => {
let mut arg_val = Arg::new(arg_name.clone())
.env(arg_name.to_uppercase().replace(".", "_"))
.long(arg_name);
if let Some(true) = arg_flag.required {
arg_val = arg_val.required(true);
}
if let Some(def) = &arg_flag.default_value {
arg_val = arg_val.default_value(def);
}
if let Some(desc) = &arg_flag.description {
arg_val = arg_val.help(&*desc.clone().leak())
}
cmd.arg(arg_val)
}
};
}
}
}
crate::model::CuddleScript::Dagger(_) => todo!(),
crate::model::CuddleScript::Lua(l) => {}
crate::model::CuddleScript::Rust(_) => todo!(),
}
cmds.push(cmd)
}
cmds
}
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(action_matches, cli.variables.clone())?;
Ok(())
}
_ => Err(anyhow::anyhow!("could not find a match")),
}
}
_ => Err(anyhow::anyhow!("could not find a match")),
}
}

View File

@@ -5,7 +5,10 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks}; use git2::{
build::{CheckoutBuilder, RepoBuilder},
FetchOptions, RemoteCallbacks,
};
use crate::{ use crate::{
config::{CuddleConfig, CuddleFetchPolicy}, config::{CuddleConfig, CuddleFetchPolicy},
@@ -25,7 +28,9 @@ pub struct CuddleContext {
pub node_type: CuddleTreeType, pub node_type: CuddleTreeType,
} }
pub fn extract_cuddle(config: CuddleConfig) -> anyhow::Result<Arc<Mutex<Vec<CuddleContext>>>> { pub fn extract_cuddle(
config: CuddleConfig,
) -> anyhow::Result<Option<Arc<Mutex<Vec<CuddleContext>>>>> {
let mut curr_dir = current_dir()?; let mut curr_dir = current_dir()?;
curr_dir.push(".cuddle/"); curr_dir.push(".cuddle/");
let fetch_policy = config.get_fetch_policy()?; let fetch_policy = config.get_fetch_policy()?;
@@ -39,6 +44,10 @@ pub fn extract_cuddle(config: CuddleConfig) -> anyhow::Result<Arc<Mutex<Vec<Cudd
// Load main cuddle file. // Load main cuddle file.
let cuddle_yaml = find_root_cuddle()?; let cuddle_yaml = find_root_cuddle()?;
if cuddle_yaml.is_none() {
return Ok(None);
}
let cuddle_yaml = cuddle_yaml.unwrap();
log::trace!(cuddle_yaml=log::as_debug!(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())?; let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(cuddle_yaml.as_str())?;
@@ -74,7 +83,7 @@ pub fn extract_cuddle(config: CuddleConfig) -> anyhow::Result<Arc<Mutex<Vec<Cudd
} }
} }
Ok(context) Ok(Some(context))
} }
fn create_cuddle_local() -> anyhow::Result<PathBuf> { fn create_cuddle_local() -> anyhow::Result<PathBuf> {
@@ -111,19 +120,27 @@ fn pull_parent_cuddle_into_local(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut rc = RemoteCallbacks::new(); let mut rc = RemoteCallbacks::new();
rc.credentials(|_url, username_from_url, _allowed_types| { rc.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key( if *"true" == std::env::var("CUDDLE_SSH_AGENT").ok().unwrap_or("".into()) {
username_from_url.unwrap(), git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
None, } else {
Path::new(&format!("{}/.ssh/id_ed25519", env::var("HOME").unwrap())), git2::Cred::ssh_key(
None, username_from_url.unwrap(),
) None,
Path::new(&format!("{}/.ssh/id_ed25519", env::var("HOME").unwrap())),
None,
)
}
}); });
rc.certificate_check(|_cert, _something| Ok(git2::CertificateCheckStatus::CertificateOk));
let mut fo = FetchOptions::new(); let mut fo = FetchOptions::new();
fo.remote_callbacks(rc); fo.remote_callbacks(rc);
let co = CheckoutBuilder::new();
RepoBuilder::new() RepoBuilder::new()
.fetch_options(fo) .fetch_options(fo)
.with_checkout(co)
.clone(&parent_cuddle, &destination)?; .clone(&parent_cuddle, &destination)?;
log::debug!(parent_cuddle=log::as_display!(parent_cuddle); "pulled repository"); log::debug!(parent_cuddle=log::as_display!(parent_cuddle); "pulled repository");
@@ -131,9 +148,15 @@ fn pull_parent_cuddle_into_local(
Ok(()) Ok(())
} }
fn recurse_parent(path: PathBuf, context: Arc<Mutex<Vec<CuddleContext>>>) -> anyhow::Result<()> { fn recurse_parent(
path: PathBuf,
context: Arc<Mutex<Vec<CuddleContext>>>,
) -> anyhow::Result<Option<()>> {
let cuddle_contents = find_cuddle(path.clone())?; let cuddle_contents = find_cuddle(path.clone())?;
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents)?; if cuddle_contents.is_none() {
return Ok(None);
}
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents.unwrap())?;
let ctx = context.clone(); let ctx = context.clone();
if let Ok(mut ctxs) = ctx.lock() { if let Ok(mut ctxs) = ctx.lock() {
@@ -147,14 +170,12 @@ fn recurse_parent(path: PathBuf, context: Arc<Mutex<Vec<CuddleContext>>>) -> any
} }
match cuddle_plan.base { match cuddle_plan.base {
CuddleBase::Bool(true) => { CuddleBase::Bool(true) => Err(anyhow::anyhow!(
return Err(anyhow::anyhow!( "plan cannot be enabled without specifying a plan"
"plan cannot be enabled without specifying a plan" )),
))
}
CuddleBase::Bool(false) => { CuddleBase::Bool(false) => {
log::debug!("plan is root: finishing up"); log::debug!("plan is root: finishing up");
return Ok(()); Ok(Some(()))
} }
CuddleBase::String(parent_plan) => { CuddleBase::String(parent_plan) => {
let destination_path = create_cuddle(path.clone())?; let destination_path = create_cuddle(path.clone())?;
@@ -164,29 +185,27 @@ fn recurse_parent(path: PathBuf, context: Arc<Mutex<Vec<CuddleContext>>>) -> any
if !cuddle_dest.exists() { if !cuddle_dest.exists() {
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?; pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
} }
return recurse_parent(cuddle_dest, context.clone()); recurse_parent(cuddle_dest, context.clone())
} }
} }
} }
fn find_root_cuddle() -> anyhow::Result<String> { fn find_root_cuddle() -> anyhow::Result<Option<String>> {
// TODO: Make recursive towards root // TODO: Make recursive towards root
let current_dir = env::current_dir()?; let current_dir = env::current_dir()?;
find_cuddle(current_dir) find_cuddle(current_dir)
} }
fn find_cuddle(path: PathBuf) -> anyhow::Result<String> { fn find_cuddle(path: PathBuf) -> anyhow::Result<Option<String>> {
for entry in std::fs::read_dir(path)? { for entry in std::fs::read_dir(path)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
let metadata = std::fs::metadata(&path)?; let metadata = std::fs::metadata(&path)?;
if metadata.is_file() && path.file_name().unwrap() == OsStr::new("cuddle.yaml") { if metadata.is_file() && path.file_name().unwrap() == OsStr::new("cuddle.yaml") {
return Ok(std::fs::read_to_string(path)?); return Ok(Some(std::fs::read_to_string(path)?));
} }
} }
Err(anyhow::anyhow!( Ok(None)
"Could not find 'cuddle.yaml' in the current directory"
))
} }

View File

@@ -1,5 +1,6 @@
use config::CuddleConfig; use config::CuddleConfig;
use tracing::Level; use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
mod actions; mod actions;
mod cli; mod cli;
@@ -10,6 +11,7 @@ mod util;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
init_logging()?; init_logging()?;
let _ = dotenvy::dotenv();
let config = CuddleConfig::from_env()?; let config = CuddleConfig::from_env()?;
@@ -20,9 +22,9 @@ fn main() -> anyhow::Result<()> {
} }
fn init_logging() -> anyhow::Result<()> { fn init_logging() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::registry()
.pretty() .with(fmt::layer())
.with_max_level(Level::INFO) .with(EnvFilter::from_default_env())
.init(); .init();
Ok(()) Ok(())

238
cuddle/src/model.rs Normal file
View File

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

View File

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

View File

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

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,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,6 +1,6 @@
#!/bin/bash #!/bin/bash
CUDDLE_FETCH_POLICY=never cuddle_cli render_template \ CUDDLE_FETCH_POLICY=never cuddle render_template \
--template-file "$TMP/input.txt.tmpl" \ --template-file "$TMP/input.txt.tmpl" \
--destination "$TMP/input.txt" \ --destination "$TMP/input.txt" \
--extra-var "extravar=someextravar" --extra-var "extravar=someextravar"

3
renovate.json Normal file
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false, "additionalProperties": true,
"properties": { "properties": {
"base": { "base": {
"title": "Base url from which to base current cuddle plan on", "title": "Base url from which to base current cuddle plan on",
@@ -27,6 +27,9 @@
}, },
{ {
"type": "object" "type": "object"
},
{
"type": "array"
} }
] ]
} }
@@ -70,6 +73,32 @@
"patternProperties": { "patternProperties": {
"^.*$": { "^.*$": {
"anyOf": [ "anyOf": [
{
"type": "object",
"required": [
"type",
"name"
],
"properties": {
"type": {
"enum": [
"flag"
]
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"default": {
"type": "string"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@@ -83,6 +112,9 @@
"env" "env"
] ]
}, },
"description": {
"type": "string"
},
"key": { "key": {
"title": "the environment key to pull arg from", "title": "the environment key to pull arg from",
"type": "string" "type": "string"

5
scripts/install.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
cargo install --path cuddle/ --force
cuddle --version

View File

@@ -1,4 +1,4 @@
FROM rust:1.62.1-slim-bullseye as base FROM rustlang/rust:nightly as base
RUN rustup target add x86_64-unknown-linux-musl RUN rustup target add x86_64-unknown-linux-musl
@@ -14,10 +14,25 @@ RUN apt-get install -y -q build-essential curl git
WORKDIR /app/cuddle/ WORKDIR /app/cuddle/
RUN cargo install --target x86_64-unknown-linux-musl --git https://git.front.kjuulh.io/kjuulh/cuddle.git cuddle_cli COPY . .
FROM docker:stable-dind RUN cargo install --target x86_64-unknown-linux-musl --path cuddle
FROM docker:dind
RUN apk add bash git kubectl
ENV SLICE_VERSION=v1.2.7
RUN wget -O kubectl-slice_linux_x86_64.tar.gz \
"https://github.com/patrickdappollonio/kubectl-slice/releases/download/$SLICE_VERSION/kubectl-slice_linux_x86_64.tar.gz" && \
tar -xf kubectl-slice_linux_x86_64.tar.gz && \
chmod +x ./kubectl-slice && \
mv ./kubectl-slice /usr/local/bin/kubectl-slice && \
rm kubectl-slice_linux_x86_64.tar.gz
RUN eval `ssh-agent`
COPY --from=1password/op:2 /usr/local/bin/op /usr/local/bin/op
COPY --from=base /usr/local/cargo/bin/ /usr/local/cargo/bin/ COPY --from=base /usr/local/cargo/bin/ /usr/local/cargo/bin/
ENV PATH="${PATH}:/usr/local/cargo/bin" ENV PATH="${PATH}:/usr/local/cargo/bin"

View File

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