Compare commits

..

243 Commits
v0.1.0 ... main

Author SHA1 Message Date
b43c322dbd chore(deps): update rust crate serde_json to v1.0.133
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-17 05:24:58 +00:00
453f405124
feat: remove deps
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 17:11:30 +01:00
c05442baf6
feat: remove ci
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 17:11:06 +01:00
cuddle-please
811969871f chore(release): 0.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-11-16 16:08:05 +00:00
c8b8328ce2
feat: with lib drone
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 17:06:07 +01:00
7932e7a913
feat: with rust something
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 17:05:16 +01:00
c8f56874d0
feat: fix errors
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 16:09:07 +01:00
66efb97120
feat: update dagger
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-16 15:59:59 +01:00
ca2f490f6d chore(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:24:07 +00:00
34b081d004 chore(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:30:12 +00:00
d902fc186e chore(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:31:07 +00:00
4685dd28f9
feat: update
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:34:34 +02:00
6aa5686a0d chore(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:23:53 +00:00
eb66d51984 chore(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:49:04 +00:00
fe665a61a2 chore(deps): update rust crate serde to v1.0.208
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-21 20:52:10 +00:00
171fa0e6fa
feat: without extra packages
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-18 12:02:23 +02:00
90598c9ffc
feat: wrong exclude
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-07 11:31:11 +02:00
7800794271
feat: also exclude tests
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-07 11:24:26 +02:00
12dff478a3
feat: update dagger 0.11.10
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-01 22:53:04 +02:00
a7656a9da1
feat: update dagger 0.11.7
Some checks reported errors
continuous-integration/drone/push Build was killed
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-08-01 22:19:59 +02:00
f11687dc26
feat: add empty cuddle please for now
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-06-01 14:40:47 +02:00
2eef711f9d
feat: update
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 15:49:41 +02:00
9996fb742a
feat: update a lot
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 15:47:26 +02:00
6dbd681d67 chore(deps): update rust crate serde to v1.0.203
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-25 21:17:21 +00:00
4bd6032c05
feat: update components on prs to also build release
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 23:10:11 +02:00
65735ba15a fix(deps): update rust crate chrono to 0.4.38
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-15 10:15:17 +00:00
5c580691dd chore(deps): update rust crate async-trait to 0.1.80
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2024-04-11 19:09:17 +00:00
fa6654bb3b
feat: update version
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-09 23:29:13 +02:00
3155e75240
feat: update image
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-09 23:03:42 +02:00
82a7b7e9b1
feat: move to user local bin
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 23:04:06 +02:00
556cb152e8
feat: with alpine instead
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:56:45 +02:00
372b7a2526
feat: add permissions
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:49:42 +02:00
c4e3e5781c
feat: use version as well
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:42:35 +02:00
a38906466e
feat: install curl
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:40:23 +02:00
d115d57129
fix: typo
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:38:17 +02:00
4141079d8c
feat: trying again
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:36:00 +02:00
ff55c99279
feat: add dagger bin actually6 compiles
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:33:00 +02:00
7d586337e9
feat: add dagger bin
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 22:29:06 +02:00
48f142ef58
feat: add file
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-06 21:53:09 +02:00
59748fb6e6
feat: match values first
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 23:08:05 +02:00
62d54ce56f
feat: pretty print
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 23:06:51 +02:00
5cac7d1c88
feat: with debug
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 23:04:37 +02:00
8b080f2f43
feat: return with sqlx
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 23:01:11 +02:00
2daa0864e2
feat: add debug logs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 22:58:25 +02:00
c3f9613541
feat: with single migrations
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 22:55:07 +02:00
924bcf8c8c
feat: handle for cuddle_file
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-05 22:45:25 +02:00
99bc420f71
feat: with cuddle release clone
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:48:20 +01:00
5fb1399f0c
feat: use stuff
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:40:46 +01:00
c0fe864923
feat: with as ref as well
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:39:14 +01:00
6ec6844ebb
feat: implement clone
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:34:23 +01:00
8690a06aa4
feat: now with to owned as well
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:33:44 +01:00
20ea9fd3c6
feat: use generics
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 23:18:56 +01:00
c38062bb46
feat: use cuddle please
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 22:54:56 +01:00
c03f351d81
refactor: split module
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 22:53:09 +01:00
823712e2bf
feat: with rust workspace members
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 22:33:25 +01:00
0539e375b1
feat: update dagger
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 21:17:29 +01:00
a6dab9a178
feat: fix cuddle_releaser
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 21:03:37 +01:00
f4e7ced9d8
feat: make cloneable
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:49:10 +01:00
4f15e5a887
feat: remove extra fluff
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:42:41 +01:00
e7e456e5d9
feat: add trace
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:37:07 +01:00
250807d16f
feat: add image tag
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:30:59 +01:00
e479c79cc9
feat: make sure to add values property
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:26:13 +01:00
9aa3e88b32
feat: update image
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 15:13:15 +01:00
0d744623ce
feat: add system time
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 14:33:36 +01:00
5234c1da89
feat: update drone templater
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 14:25:00 +01:00
b9417747a8
feat: include pipeline
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 02:26:08 +01:00
365b840fe9
feat: include sync
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:56:57 +01:00
ef77451bb7
feat: use self.client
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:48:26 +01:00
a6d83daf4d
feat: with aborting
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:47:57 +01:00
29c59f1ca6
feat: fix errors
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:46:54 +01:00
8282f89640
feat: fix errors
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:46:25 +01:00
9ba98284ae
feat: create drone templater action
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-30 01:41:33 +01:00
d9ea6162d4 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-28 17:31:15 +00:00
95d6704fc1 chore(deps): update rust crate async-trait to 0.1.79
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-24 03:49:51 +00:00
22f15505aa chore(deps): update rust crate async-trait to 0.1.78
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-17 00:18:24 +00:00
d1285aa7ee fix(deps): update rust crate chrono to 0.4.35
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-06 14:09:46 +00:00
9b4c3a61e1
feat: make cuddle_releaser great again
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 16:54:44 +01:00
378d60e943
feat: upgrade services to bookworm
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 16:22:42 +01:00
359222b281
feat: upgrade to bookworm
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 16:13:11 +01:00
d2467529b3
feat: with apt
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 16:07:05 +01:00
d42bd82f76
feat: with logging
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 15:52:10 +01:00
11f6fdb1d5
feat: with logging
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 15:51:36 +01:00
34145ba380
feat: update leptos service
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-03-02 15:23:54 +01:00
ac435a843e
feat: rerun blabla
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-11 14:24:20 +01:00
6a47528f11
feat: with export
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-11 14:16:30 +01:00
999cc9d59f
feat: with fix
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-11 14:13:27 +01:00
5dbaf3c87f
feat: cargo update
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-11 14:11:44 +01:00
90692d026d
feat: with ignore sub source as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-11 14:11:24 +01:00
0ad738700a fix(deps): update rust crate chrono to 0.4.34
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-11 05:59:00 +00:00
49d0da5e82
feat: add helm to kubectl
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-06 21:42:51 +01:00
122d453387
feat: add main
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 21:09:08 +01:00
018d21872a
feat: add timestamp
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 21:00:15 +01:00
d471a6190a
feat: use tag instead
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 20:42:23 +01:00
2735b7d2cf
feat: use tag instead
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 20:42:17 +01:00
96ea5e83f5
feat: revert
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 19:57:55 +01:00
385e4ab771
feat: from cuddle file up
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 19:55:08 +01:00
a62a88915e
feat: remember to split output string
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 19:39:05 +01:00
e475550004
feat: with cuddle x render args
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 19:02:03 +01:00
512c3f625e
feat: with context
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-02-03 18:54:17 +01:00
e8507cd2f2 chore(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:40:25 +00:00
5465f1eef7 chore(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 20:54:54 +00:00
f62b90dd3b
feat: with kubeslice
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 21:17:16 +01:00
f0a719e3ed
feat: with kubeslice
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 21:16:48 +01:00
91ff1f17ae
feat: add kubectl command
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:55:24 +01:00
c16396db8c
feat: trying spawn
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:37:45 +01:00
c016752e4f
feat: trying std::process instead
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:34:46 +01:00
2611b6c7f9
feat: stderr pipes
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:32:38 +01:00
050d599d81
feat: with nonzero exit code
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:26:32 +01:00
0deb6f83a0
feat: with more logs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 20:11:36 +01:00
f2196920f4
feat: update releaser
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-31 19:50:16 +01:00
cc13070f77 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-28 23:31:42 +00:00
ab9e764cc3 fix(deps): update rust crate futures to 0.3.30
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-28 23:04:24 +00:00
3f8aa88617
feat: disable ci for now
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 23:43:30 +01:00
050b81c6ae
feat: add cuddle_cli
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 23:42:54 +01:00
2b4a67e38c
feat: add cuddle_cli
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 23:41:59 +01:00
7363871ffb
feat: with docker
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 23:34:05 +01:00
a2250edadb
feat: with apt before package
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 23:24:33 +01:00
14ab05eb9b
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:59:32 +01:00
2cb8929b91
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:58:23 +01:00
1f23425150
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:57:37 +01:00
c40b74513d
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:54:49 +01:00
f5b1f2a1d8
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:52:17 +01:00
068ad47598
feat: all the logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:45:25 +01:00
bc4bc3df94
feat: with new image
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:43:31 +01:00
ca237f2ee3
feat: with new image
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:34:51 +01:00
46c88f1e6f
feat: with new image
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:28:35 +01:00
42c968f460
feat: set rust log error
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:25:59 +01:00
f882f56c9c
feat: add time str
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:24:28 +01:00
1fa94aa5ff
feat: new image
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:18:51 +01:00
19ad63d31d
feat: without logs
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:12:46 +01:00
fa67d71d64
feat: try again
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:12:35 +01:00
1aa9d1993b
feat: with updated releaser
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 22:03:17 +01:00
f86c19c510
feat: with trace
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:56:46 +01:00
5875c96aa8
feat: with output
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:49:52 +01:00
663fe0b199
feat: use proper releaser
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:46:05 +01:00
7a1f04a071
feat: add auth sock
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:19:02 +01:00
8788f78f81
feat: with cuddle x render
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 21:06:16 +01:00
c0ae03376b
feat: add deployment take 2
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 20:45:25 +01:00
3da154882d
feat: add deployment
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 20:44:12 +01:00
5378b5f537
feat: add releaser
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 19:20:38 +01:00
926199a487
feat: conditionally disable deployment
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-01-28 00:25:42 +01:00
3a6f5bb6d2
feat: without home
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:51:26 +01:00
fb4c064bec
feat: run before base
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:50:18 +01:00
3ff9cff6d6
fix: as isize
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:45:06 +01:00
84626939e7
feat: set user
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:43:53 +01:00
638117e7e9
feat: with empty string
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:41:56 +01:00
877f19aa72
fix: actually build the builder
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:40:49 +01:00
7daf873e37
feat: with ingored host key checking
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:40:08 +01:00
cf05cf4228
feat: with migrations as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:18:13 +01:00
97b70164d1
feat: with opinionated ssh auth sock fetch
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:07:44 +01:00
830e39e8bf
feat: set env variable as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 22:00:00 +01:00
8248b61545
chore: rename with_socket to with_ssh_agent
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 21:59:21 +01:00
9ee64020fe
fix: build errors on ssh agent
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 21:58:17 +01:00
c759db28b0
feat: with ssh agent
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-30 21:55:29 +01:00
e67c4baa97
feat: extract cuddle_please
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-29 19:55:10 +01:00
1b5ee9b7e3
feat: update image
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-29 19:52:27 +01:00
80b99c27a5
feat: with cuddle please
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-29 19:43:04 +01:00
124aa93b98
feat: without new async
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-29 17:19:22 +01:00
10ef2294d2
feat: with rust_lib
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-29 17:18:02 +01:00
1e257b1269
feat: with main as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-28 14:31:32 +01:00
97bf2b217c
feat: with test with leptos
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-28 14:26:55 +01:00
0e7f134bd0
feat: add initial leptos
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-28 14:17:31 +01:00
c977fdbcaa
feat: add postgresql-dev
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 18:07:18 +01:00
63fbecd194
feat: without nodemodules
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 17:34:17 +01:00
d352ace1ea
feat: with entry point
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 17:30:14 +01:00
08c36e737c
feat: with actual pr
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 17:25:18 +01:00
8b929c912c
feat: with pub fn new
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 17:22:46 +01:00
a78803d35b
feat: add node service
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-26 17:20:41 +01:00
3cdd876e05
feat: trying again with opts
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-01 22:36:44 +01:00
ec2a0f5779
feat: without opts
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-01 22:12:59 +01:00
4e380b17c0
feat: set registry
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-01 21:59:26 +01:00
88a467780c
feat: move to after package
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-01 21:42:58 +01:00
bcdfaf2b2d
feat: with docker cache
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-01 21:41:13 +01:00
94efc99d05
feat: update assets
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-28 11:27:15 +01:00
320ff343e6
feat: with assets
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-28 10:12:19 +01:00
67803d315c
feat: with package as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-28 01:02:04 +01:00
5908cd9526
feat: without deps
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-28 00:51:29 +01:00
62f66452c2
feat: with ca certificates
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-28 00:49:28 +01:00
a5d08f4a0d
feat: with working ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 23:28:35 +01:00
12b7c27342
feat: update with ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 22:32:27 +01:00
9c3f806a80
feat: with ssh sock dep
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 22:18:39 +01:00
663feba85d
feat: can use ssh sock
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 22:18:05 +01:00
e5b3e1b62a
feat: with username
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 22:15:11 +01:00
0f7ed2b6f4
feat: with git name
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 22:04:39 +01:00
c391482874
feat: with sync
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 21:36:46 +01:00
b879da4d2f
feat: with update deployment
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 21:34:23 +01:00
a900ebae54
feat: with registry
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 21:24:45 +01:00
c893dc9005
feat: with before test
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 20:31:29 +01:00
30587b2f97
feat: with impl into
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 20:19:50 +01:00
bda242422d
feat: with arc
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 20:11:49 +01:00
999d81bb7a
feat: with &mut service
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 19:20:41 +01:00
93e73cc66e
feat: with mutex
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 19:14:31 +01:00
52266599e2
feat: with rust service impl
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 18:11:58 +01:00
85ad929d80
feat: with src
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 14:15:43 +01:00
a0acb54896
feat: with sqlx
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 13:52:11 +01:00
2a988c33a4
feat: forgot async_trait
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 13:44:44 +01:00
015cb6b23a
feat: with cargo clean
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 13:42:13 +01:00
8869b75072
feat: with extensions
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-27 13:25:31 +01:00
3c28b30f8f
feat: extract arch
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-26 22:46:34 +01:00
7a1ad63b57
feat: with full support for rust services
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-26 22:19:34 +01:00
80782e70f9
chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:16:21 +01:00
3939940c01
chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:14:38 +01:00
82ccdefd93
feat: with middleware
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:10:09 +01:00
3e9a840851 chore(deps): update rust crate async-scoped to 0.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-25 21:26:21 +00:00
455660f1e0 chore(deps): update rust crate futures to 0.3.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-25 21:01:05 +00:00
f5ba46186b
feat: with logs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 21:49:04 +01:00
8cf148726a
feat: add cuddle ci draft
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 21:41:17 +01:00
e29615cb05
feat: with offline mode
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 14:00:17 +01:00
cdd13283e0
feat: with cargo clean
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:50:43 +01:00
a3d92cdde3
feat: without export
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:47:17 +01:00
e4fc1cc834
feat: with output
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:33:54 +01:00
d6af354776
feat: with nested mold
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:55:56 +01:00
0524b2e0bf
feat: fix name
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:48:57 +01:00
5c69c3fa16
feat: with mold
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:47:43 +01:00
10956e7af4
feat: with mold
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:44:26 +01:00
4f72b4fdae
feat: with htmx
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 11:58:22 +01:00
ec029c81db chore(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:40:10 +00:00
ddde6c0734 fix(deps): update rust crate async-scoped to 0.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-16 05:30:17 +00:00
cc1c356ad0 chore(deps): update rust crate tokio to 1.34.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-09 20:12:11 +00:00
e18d247e11 fix(deps): update rust crate futures to 0.3.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 15:02:32 +00:00
11323c0752
feat: add leptos
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-22 12:09:57 +02:00
39d15b5d7f chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-21 11:39:12 +00:00
bdaea19ac6
feat: ignore cache
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 13:36:51 +02:00
2d57b4f3b4
feat: update lock
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 13:15:24 +02:00
52914e08e6
feat: with updated dagger-sdk
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 12:45:33 +02:00
e2c7f46378
fix(git): make sure we actually fail when running an invalid git command
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:47:29 +02:00
82289f2552
chore: with version 0.2.0
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:37:22 +02:00
2482987daf
chore: publish
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:36:50 +02:00
614a3bc305
feat(rust-publish): with rust publish
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:29:24 +02:00
5e604d7a10
chore: add noop release script
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:22:47 +02:00
cuddle-please
a94fd3e68e chore(release): 0.2.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2023-08-12 19:14:42 +00:00
e2f1d79031
fix(ci): only set local url instead of insteadOf
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:12:45 +02:00
07c593bb08
fix(ci): trim remote_url newlines
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:00:58 +02:00
61c34b9fb8
fix(ci): trim remote_url newlines
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:57:21 +02:00
03784be431 fix(ci): repo should be ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:54:06 +00:00
1cf349c3c6
fix(ci): make sure to run ssh as user git
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:26:13 +02:00
7277e06c0b
chore: set fixed versions
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:14:27 +02:00
776db7274a fix: ci
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:08:04 +00:00
e1428a8fbb feat: with rust build and test
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:08:04 +00:00
a17e527b91 chore(deps): update rust crate tokio to 1.31.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-12 16:53:55 +00:00
5c53589c27
refactor(ci): move cuddle please image to cuddle
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 12:26:12 +02:00
8c3b5e660f feat(ci): with internal please action
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-11 18:23:05 +00:00
37054fa012 Add renovate.json
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-11 18:22:57 +00:00
99 changed files with 10622 additions and 3112 deletions

View File

@ -1,170 +1,2 @@
kind: pipeline
name: default
type: docker
steps:
- name: build ci
image: rustlang/rust:nightly
volumes:
- name: ci
path: /mnt/ci
environment:
PKG_CONFIG_SYSROOT_DIR: "/"
CI_PREFIX: "/mnt/ci"
commands:
- set -e
- apt update
- apt install musl-tools pkg-config libssl-dev openssl build-essential musl-dev -y
- rustup target add x86_64-unknown-linux-musl
- cargo build --target=x86_64-unknown-linux-musl -p ci
#- cargo build -p ci
- mv target/x86_64-unknown-linux-musl/debug/ci "$CI_PREFIX/ci"
#- mv target/debug/ci $CI_PREFIX/ci
- name: load_secret
image: debian:buster-slim
volumes:
- name: ssh
path: /root/.ssh/
environment:
SSH_KEY:
from_secret: gitea_id_ed25519
commands:
- mkdir -p $HOME/.ssh/
- echo "$SSH_KEY" | base64 -d > $HOME/.ssh/id_ed25519
- name: build pr
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
- name: ci
path: /mnt/ci
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- echo "$DOCKER_PASSWORD" | docker login --password-stdin --username="$DOCKER_USERNAME" docker.io
- ldd $CI_PREFIX
- apk add git
- cuddle x ci:pr
environment:
DOCKER_BUILDKIT: 1
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_USERNAME:
from_secret: docker_username
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CI_PREFIX: "/mnt/ci/ci"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- pull_request
exclude:
- main
- master
depends_on:
- "load_secret"
- "build ci"
- name: build main
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
- name: ci
path: /mnt/ci
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- echo "$DOCKER_PASSWORD" | docker login --password-stdin --username="$DOCKER_USERNAME" docker.io
- ldd $CI_PREFIX
- apk add git
- cuddle x ci:main
environment:
DOCKER_BUILDKIT: 1
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_USERNAME:
from_secret: docker_username
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CI_PREFIX: "/mnt/ci/ci"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- push
branch:
- main
- master
exclude:
- pull_request
depends_on:
- "load_secret"
- "build ci"
- name: deploy release
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- cuddle x build:release:all
- cuddle x deploy:docs:preview
environment:
DOCKER_BUILDKIT: 1
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CUDDLE_CI: "true"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- tag
ref:
include:
- refs/tags/v*
depends_on:
- "load_secret"
- "build ci"
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: ssh
temp: {}
- name: dockersock
temp: {}
- name: ci
temp: {}
kind: template
load: cuddle-rust-lib-plan.yaml

1
.gitignore vendored
View File

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

View File

@ -6,6 +6,261 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.3.0] - 2024-11-16
### Added
- with lib drone
- with rust something
- fix errors
- update dagger
- update
- without extra packages
- wrong exclude
- also exclude tests
- update dagger 0.11.10
- update dagger 0.11.7
- add empty cuddle please for now
- update
- update a lot
- update components on prs to also build release
- update version
- update image
- move to user local bin
- with alpine instead
- add permissions
- use version as well
- install curl
- trying again
- add dagger bin actually6 compiles
- add dagger bin
- add file
- match values first
- pretty print
- with debug
- return with sqlx
- add debug logs
- with single migrations
- handle for cuddle_file
- with cuddle release clone
- use stuff
- with as ref as well
- implement clone
- now with to owned as well
- use generics
- use cuddle please
- with rust workspace members
- update dagger
- fix cuddle_releaser
- make cloneable
- remove extra fluff
- add trace
- add image tag
- make sure to add values property
- update image
- add system time
- update drone templater
- include pipeline
- include sync
- use self.client
- with aborting
- fix errors
- fix errors
- create drone templater action
- make cuddle_releaser great again
- upgrade services to bookworm
- upgrade to bookworm
- with apt
- with logging
- with logging
- update leptos service
- rerun blabla
- with export
- with fix
- cargo update
- with ignore sub source as well
- add helm to kubectl
- add main
- add timestamp
- use tag instead
- use tag instead
- revert
- from cuddle file up
- remember to split output string
- with cuddle x render args
- with context
- with kubeslice
- with kubeslice
- add kubectl command
- trying spawn
- trying std::process instead
- stderr pipes
- with nonzero exit code
- with more logs
- update releaser
- disable ci for now
- add cuddle_cli
- add cuddle_cli
- with docker
- with apt before package
- all the logs
- all the logs
- all the logs
- all the logs
- all the logs
- all the logs
- with new image
- with new image
- with new image
- set rust log error
- add time str
- new image
- without logs
- try again
- with updated releaser
- with trace
- with output
- use proper releaser
- add auth sock
- with cuddle x render
- add deployment take 2
- add deployment
- add releaser
- conditionally disable deployment
- without home
- run before base
- set user
- with empty string
- with ingored host key checking
- with migrations as well
- with opinionated ssh auth sock fetch
- set env variable as well
- with ssh agent
- extract cuddle_please
- update image
- with cuddle please
- without new async
- with rust_lib
- with main as well
- with test with leptos
- add initial leptos
- add postgresql-dev
- without nodemodules
- with entry point
- with actual pr
- with pub fn new
- add node service
- trying again with opts
- without opts
- set registry
- move to after package
- with docker cache
- update assets
- with assets
- with package as well
- without deps
- with ca certificates
- with working ssh
- update with ssh
- with ssh sock dep
- can use ssh sock
- with username
- with git name
- with sync
- with update deployment
- with registry
- with before test
- with impl into
- with arc
- with &mut service
- with mutex
- with rust service impl
- with src
- with sqlx
- forgot async_trait
- with cargo clean
- with extensions
- extract arch
- with full support for rust services
- with middleware
- with logs
- add cuddle ci draft
- with offline mode
- with cargo clean
- without export
- with output
- with nested mold
- fix name
- with mold
- with mold
- with htmx
- add leptos
- ignore cache
- update lock
- with updated dagger-sdk
- *(rust-publish)* with rust publish
### Fixed
- *(deps)* update rust crate chrono to 0.4.38
- typo
- *(deps)* update all dependencies
- *(deps)* update rust crate chrono to 0.4.35
- *(deps)* update rust crate chrono to 0.4.34
- *(deps)* update all dependencies
- *(deps)* update rust crate futures to 0.3.30
- as isize
- actually build the builder
- build errors on ssh agent
- *(deps)* update rust crate async-scoped to 0.8.0
- *(deps)* update rust crate futures to 0.3.29
- *(git)* make sure we actually fail when running an invalid git command
### Other
- *(deps)* update rust crate serde to v1.0.215
- *(deps)* update rust crate serde to v1.0.214
- *(deps)* update rust crate serde to v1.0.213
- *(deps)* update rust crate serde to v1.0.210
- *(deps)* update rust crate serde to v1.0.209
- *(deps)* update rust crate serde to v1.0.208
- *(deps)* update rust crate serde to v1.0.203
- *(deps)* update rust crate async-trait to 0.1.80
- split module
- *(deps)* update rust crate async-trait to 0.1.79
- *(deps)* update rust crate async-trait to 0.1.78
- *(deps)* update rust crate tokio to 1.36.0
- *(deps)* update rust crate eyre to 0.6.12
- rename with_socket to with_ssh_agent
- fmt
- fmt
- *(deps)* update rust crate async-scoped to 0.8.0
- *(deps)* update rust crate futures to 0.3.29
- *(deps)* update rust crate eyre to 0.6.9
- *(deps)* update rust crate tokio to 1.34.0
- *(deps)* update all dependencies
- with version 0.2.0
- publish
- add noop release script
## [0.2.0] - 2023-08-12
### Added
- with rust build and test
- *(ci)* with internal please action
### Fixed
- *(ci)* only set local url instead of insteadOf
- *(ci)* trim remote_url newlines
- *(ci)* trim remote_url newlines
- *(ci)* repo should be ssh
- *(ci)* make sure to run ssh as user git
- ci
### Other
- set fixed versions
- *(deps)* update rust crate tokio to 1.31.0
- *(ci)* move cuddle please image to cuddle
- Add renovate.json
## [0.1.0] - 2023-08-11
### Added

1623
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,30 @@
[workspace]
members = [
"crates/*",
"examples/*",
"ci"
]
members = ["crates/*", "examples/*"]
resolver = "2"
[workspace.package]
version = "0.3.0"
edition = "2021"
license = "MIT"
authors = ["kjuulh <contact@kjuulh.io>"]
readme = "README.md"
repository = "https://git.front.kjuulh.io/kjuulh/dagger-components"
[workspace.dependencies]
cuddle-components = { path = "crates/cuddle-components" }
dagger-components = { path = "crates/dagger-components" }
dagger-cuddle-please = { path = "crates/dagger-cuddle-please" }
ci = {path = "ci"}
dagger-rust = { path = "crates/dagger-rust" }
dagger-sdk = "0.2.22"
eyre = "0.6.8"
tokio = "1.30.0"
dotenv = "*"
dagger-sdk = "0.13.7"
eyre = "0.6"
tokio = "1"
dotenv = "0.15.0"
async-trait = "0.1"
color-eyre = "*"
clap = { version = "4", features = ["derive"] }
futures = "0.3"
async-scoped = { version = "0.9.0", features = ["tokio", "use-tokio"] }
serde_json = { version = "1" }
serde_yaml = { version = "0.9" }
serde = { version = "1", features = ["derive"] }

1861
ci/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
[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 = "*"
eyre = "*"
color-eyre = "*"
tokio = "1"
clap = {version = "4", features = ["derive"]}
futures = "0.3.28"
async-scoped = { version = "0.7.1", features = ["tokio", "use-tokio"] }
dotenv.workspace = true

View File

@ -1,380 +0,0 @@
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use crate::please_release::run_release_please;
#[derive(Parser, Clone)]
#[command(author, version, about, long_about = None, subcommand_required = true)]
pub struct Command {
#[command(subcommand)]
commands: Commands,
#[command(flatten)]
global: GlobalArgs,
}
#[derive(Subcommand, Clone)]
pub enum Commands {
#[command(subcommand_required = true)]
Local {
#[command(subcommand)]
command: LocalCommands,
},
PullRequest {},
Main {},
Release,
}
#[derive(Subcommand, Clone)]
pub enum LocalCommands {
Test,
PleaseRelease,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum BuildProfile {
Debug,
Release,
}
#[derive(Debug, Clone, Args)]
pub struct GlobalArgs {
#[arg(long, global = true, help_heading = "Global")]
dry_run: bool,
#[arg(long, global = true, help_heading = "Global")]
rust_builder_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
source: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let _ = dotenv::dotenv();
let _ = color_eyre::install();
let client = dagger_sdk::connect().await?;
let cli = Command::parse();
match &cli.commands {
Commands::Local { command } => match command {
LocalCommands::Test => {
let base_image =
base_rust_image(client.clone(), &cli.global, &None, &"debug".into()).await?;
test::execute(client, &cli.global, base_image).await?;
}
LocalCommands::PleaseRelease => todo!(),
},
Commands::PullRequest {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) {
let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into())
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
tokio::join!(test(client.clone(), &cli),);
}
Commands::Main {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) {
let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into())
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
async fn cuddle_please(client: Arc<dagger_sdk::Query>, cli: &Command) {
run_release_please(client.clone(), &cli.global)
.await
.unwrap();
}
tokio::join!(
test(client.clone(), &cli),
cuddle_please(client.clone(), &cli)
);
}
Commands::Release => todo!(),
}
Ok(())
}
mod please_release {
use std::sync::Arc;
use crate::GlobalArgs;
pub async fn run_release_please(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<()> {
let build_image = client
.container()
.from("kasperhermansen/cuddle-please:main-1691504183");
let src = client
.git_opts(
"https://git.front.kjuulh.io/kjuulh/dagger-components",
dagger_sdk::QueryGitOpts {
experimental_service_host: None,
keep_git_dir: Some(true),
},
)
.branch("main")
.tree();
let res = build_image
.with_secret_variable(
"CUDDLE_PLEASE_TOKEN",
client
.set_secret("CUDDLE_PLEASE_TOKEN", std::env::var("CUDDLE_PLEASE_TOKEN")?)
.id()
.await?,
)
.with_workdir("/mnt/app")
.with_directory(".", src.id().await?)
.with_exec(vec![
"git",
"remote",
"set-url",
"origin",
&format!(
"https://git:{}@git.front.kjuulh.io/kjuulh/dagger-components.git",
std::env::var("CUDDLE_PLEASE_TOKEN")?
),
])
.with_exec(vec![
"cuddle-please",
"release",
"--engine=gitea",
"--owner=kjuulh",
"--repo=dagger-components",
"--branch=main",
"--api-url=https://git.front.kjuulh.io",
"--log-level=debug",
]);
let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
let please_out = res.stdout().await?;
println!("{please_out}");
let please_out = res.stderr().await?;
println!("{please_out}");
Ok(())
}
}
mod test {
use std::sync::Arc;
use crate::GlobalArgs;
pub async fn execute(
_client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
container: dagger_sdk::Container,
) -> eyre::Result<()> {
let test_image = container
.pipeline("rust:test")
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "git"])
.with_exec(vec!["cargo", "test"]);
let please_out = test_image.stdout().await?;
println!("{please_out}");
let please_out = test_image.stderr().await?;
println!("{please_out}");
test_image.exit_code().await?;
Ok(())
}
}
pub fn get_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(vec!["node_modules/", ".git/", "target/"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_skeleton_files(
client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let mut rust_crates = vec![PathBuf::from("ci")];
let mut dirs = tokio::fs::read_dir("crates").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
let mut dirs = tokio::fs::read_dir("examples").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
println!("found crates: {}", path.display());
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = client.directory();
let mut crate_names = Vec::new();
for rust_crate in rust_crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(directory, rust_crate)?;
}
Ok((directory, crate_names))
}
pub async fn base_rust_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: &Option<String>,
profile: &String,
) -> eyre::Result<dagger_sdk::Container> {
let dep_src = get_rust_dep_src(client.clone(), args).await?;
let (skeleton_files, crates) = get_rust_skeleton_files(client.clone(), args).await?;
let src = get_src(client.clone(), args)?;
let client = client.pipeline("rust_base_image");
let rust_target = match platform
.clone()
.unwrap_or("linux/amd64".to_string())
.as_str()
{
"linux/amd64" => "x86_64-unknown-linux-gnu",
"linux/arm64" => "aarch64-unknown-linux-gnu",
_ => eyre::bail!("architecture not supported"),
};
let rust_build_image = client
.container()
.from(
args.rust_builder_image
.as_ref()
.unwrap_or(&"rustlang/rust:nightly".into()),
)
.with_exec(vec!["rustup", "target", "add", rust_target])
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "jq"]);
let target_cache = client.cache_volume(format!("rust_target_{}", profile));
let mut build_options = vec!["cargo", "build", "--target", rust_target, "--workspace"];
if profile == "release" {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src.id().await?)
.with_directory("/mnt/src/", skeleton_files.id().await?)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache.id().await?);
let exclude = crates
.iter()
.filter(|c| **c != "ci")
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let exclude = exclude.iter().map(|c| c.as_str()).collect();
let incremental_dir = client.directory().with_directory_opts(
".",
rust_prebuild.directory("target").id().await?,
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo").id().await?,
)
.with_directory("target", incremental_dir.id().await?)
.with_directory("/mnt/src/", src.id().await?);
Ok(rust_with_src)
}

View File

@ -0,0 +1,36 @@
[package]
name = "cuddle-ci"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
dagger-cuddle-please.workspace = true
dagger-sdk.workspace = true
eyre.workspace = true
clap.workspace = true
async-trait.workspace = true
futures.workspace = true
tokio.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
serde.workspace = true
tracing = {version = "0.1.40", features = ["log"]}
chrono = {version = "0.4.38"}
toml = "0.8.12"
[dev-dependencies]
pretty_assertions = "1.4.0"
tokio.workspace = true
[features]
default = []
dagger = []
integration = []

198
crates/cuddle-ci/src/cli.rs Normal file
View File

@ -0,0 +1,198 @@
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
};
use async_trait::async_trait;
pub struct CuddleCI {
pr_action: Vec<Box<dyn PullRequestAction + Send + Sync>>,
main_action: Vec<Box<dyn MainAction + Send + Sync>>,
release_action: Vec<Box<dyn ReleaseAction + Send + Sync>>,
}
#[derive(Default, Debug)]
pub struct Context {
store: BTreeMap<String, String>,
}
impl Deref for Context {
type Target = BTreeMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl DerefMut for Context {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.store
}
}
impl CuddleCI {
pub fn new(
pr: Box<dyn PullRequestAction + Send + Sync>,
main: Box<dyn MainAction + Send + Sync>,
release: Box<dyn ReleaseAction + Send + Sync>,
) -> Self {
Self {
pr_action: vec![pr],
main_action: vec![main],
release_action: vec![release],
}
}
pub fn with_pull_request<T>(&mut self, pr: &T) -> &mut Self
where
T: PullRequestAction + ToOwned + Send + Sync + 'static,
T: ToOwned<Owned = T>,
{
self.pr_action.push(Box::new(pr.to_owned()));
self
}
pub fn with_main<T>(&mut self, main: &T) -> &mut Self
where
T: MainAction + Send + Sync + 'static,
T: ToOwned<Owned = T>,
{
self.main_action.push(Box::new(main.to_owned()));
self
}
pub fn with_release(&mut self, release: Box<dyn ReleaseAction + Send + Sync>) -> &mut Self {
self.release_action.push(release);
self
}
pub async fn execute(
&mut self,
args: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<()> {
let matches = clap::Command::new("cuddle-ci")
.about("is a wrapper around common CI actions")
.subcommand(clap::Command::new("pr"))
.subcommand(clap::Command::new("main"))
.subcommand(clap::Command::new("release"))
.subcommand_required(true)
.try_get_matches_from(args.into_iter().map(|a| a.into()).collect::<Vec<String>>())?;
let mut context = Context::default();
match matches.subcommand() {
Some((name, args)) => match (name, args) {
("pr", _args) => {
eprintln!("starting pr validate");
for pr_action in self.pr_action.iter() {
pr_action.execute_pull_request(&mut context).await?;
}
eprintln!("finished pr validate");
}
("main", _args) => {
eprintln!("starting main validate");
for main_action in self.main_action.iter() {
main_action.execute_main(&mut context).await?;
}
eprintln!("finished main validate");
}
("release", _args) => {
eprintln!("starting release validate");
for release_action in self.release_action.iter() {
release_action.execute_release(&mut context).await?;
}
eprintln!("finished release validate");
}
(command_name, _) => {
eyre::bail!("command is not recognized: {}", command_name)
}
},
None => eyre::bail!("command required a subcommand [pr, main, release] etc."),
}
Ok(())
}
}
impl Default for CuddleCI {
fn default() -> Self {
Self::new(
Box::new(DefaultPullRequestAction {}),
Box::new(DefaultMainAction {}),
Box::new(DefaultReleaseAction {}),
)
}
}
#[async_trait]
pub trait PullRequestAction {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
eprintln!("validate pull request: noop");
Ok(())
}
}
pub struct DefaultPullRequestAction {}
#[async_trait]
impl PullRequestAction for DefaultPullRequestAction {}
#[async_trait]
pub trait MainAction {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()>;
}
pub struct DefaultMainAction {}
#[async_trait]
impl MainAction for DefaultMainAction {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
Ok(())
}
}
#[async_trait]
pub trait ReleaseAction {
async fn execute_release(&self, _ctx: &mut Context) -> eyre::Result<()> {
eprintln!("validate release: noop");
Ok(())
}
}
pub struct DefaultReleaseAction {}
#[async_trait]
impl ReleaseAction for DefaultReleaseAction {}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn test_can_call_default() -> eyre::Result<()> {
CuddleCI::default().execute(["cuddle-ci", "pr"]).await?;
Ok(())
}
#[tokio::test]
async fn test_fails_on_no_command() -> eyre::Result<()> {
let res = CuddleCI::default().execute(["cuddle-ci"]).await;
assert!(res.is_err());
Ok(())
}
#[tokio::test]
async fn test_fails_on_wrong_command() -> eyre::Result<()> {
let res = CuddleCI::default()
.execute(["cuddle-ci", "something"])
.await;
assert!(res.is_err());
Ok(())
}
}

View File

@ -0,0 +1,273 @@
use std::{collections::BTreeMap, path::PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleFile {
pub vars: CuddleVars,
pub deployment: Option<CuddleDeployment>,
pub components: Option<CuddleComponents>,
pub please: Option<CuddlePlease>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleComponents {
pub database: Option<CuddleDatabase>,
pub assets: Option<CuddleAssets>,
pub packages: Option<Packages>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleAssets {
pub volumes: Option<Vec<CuddleAssetInclude>>,
pub clean: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddlePlease {}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Packages {
pub debian: DebianPackages,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct DebianPackages {
pub dev: Vec<String>,
pub release: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleAssetInclude {
pub from: PathBuf,
pub to: PathBuf,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum CuddleDatabase {
Enabled(bool),
Values { migrations: PathBuf },
Default {},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleVars {
pub service: String,
pub registry: String,
pub clusters: Option<CuddleClusters>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeployment {
pub registry: String,
pub env: CuddleDeploymentEnv,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeploymentEnv(pub BTreeMap<String, CuddleDeploymentCluster>);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeploymentCluster {
pub clusters: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleClusters(pub BTreeMap<String, CuddleCluster>);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleCluster {
pub namespace: String,
}
impl CuddleFile {
pub async fn from_cuddle_file() -> eyre::Result<Self> {
let cuddle_file_content = tokio::fs::read_to_string("cuddle.yaml").await?;
Self::parse_cuddle_file(&cuddle_file_content)
}
pub fn parse_cuddle_file(content: &str) -> eyre::Result<Self> {
let cuddle_file: CuddleFile = serde_yaml::from_str(content)?;
Ok(cuddle_file)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_file() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
clusters:
clank_prod:
replicas: "3"
namespace: clank_prod
deployment:
registry: git@git.front.kjuulh.io:kjuulh/clank-clusters
env:
prod:
clusters:
- clank_prod
components:
database: true
assets:
volumes:
- from: somewhere
to: somewhere-else
packages:
debian:
dev:
- "capnp"
release:
- "capnp"
scripts:
render:
type: shell
args:
cluster:
name: cluster
type: flag
image_tag:
name: image_tag
type: flag"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let mut clusters = BTreeMap::new();
clusters.insert(
"clank_prod".into(),
CuddleCluster {
namespace: "clank_prod".into(),
},
);
let mut deployment = BTreeMap::new();
deployment.insert(
"prod".into(),
CuddleDeploymentCluster {
clusters: vec!["clank_prod".into()],
},
);
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: Some(CuddleClusters(clusters)),
},
deployment: Some(crate::cuddle_file::CuddleDeployment {
registry: "git@git.front.kjuulh.io:kjuulh/clank-clusters".into(),
env: CuddleDeploymentEnv(deployment),
}),
components: Some(CuddleComponents {
database: Some(CuddleDatabase::Enabled(true)),
assets: Some(CuddleAssets {
volumes: Some(vec![CuddleAssetInclude {
from: "somewhere".into(),
to: "somewhere-else".into(),
}]),
clean: None,
}),
packages: Some(Packages {
debian: DebianPackages {
dev: vec!["capnp".into()],
release: vec!["capnp".into()],
},
}),
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
#[test]
fn cuddle_database_default() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
components:
database: {}
"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: None,
},
deployment: None,
components: Some(CuddleComponents {
database: Some(CuddleDatabase::Default {}),
assets: None,
packages: None,
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
#[test]
fn cuddle_packages() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
components:
packages:
debian:
dev:
- "capnp"
release:
- "capnp"
"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: None,
},
deployment: None,
components: Some(CuddleComponents {
database: None,
assets: None,
packages: Some(Packages {
debian: DebianPackages {
dev: vec!["capnp".into()],
release: vec!["capnp".into()],
},
}),
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
}

View File

@ -0,0 +1,37 @@
use async_trait::async_trait;
use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePlease};
use crate::{Context, MainAction};
#[derive(Clone)]
pub struct CuddlePlease {
client: dagger_sdk::Query,
}
impl CuddlePlease {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl MainAction for CuddlePlease {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
let client = self.client.clone();
let action = DaggerCuddlePlease::new(client);
action
.cuddle_please_src(&CuddlePleaseSrcArgs {
cuddle_image: "kasperhermansen/cuddle-please:main-1712698022".into(),
server: dagger_cuddle_please::models::SrcServer::Gitea {
token: std::env::var("CUDDLE_PLEASE_TOKEN")
.expect("CUDDLE_PLEASE_TOKEN to be present"),
},
log_level: Some(dagger_cuddle_please::models::LogLevel::Debug),
})
.await?;
Ok(())
}
}

View File

@ -0,0 +1,152 @@
use std::fmt::Display;
use async_trait::async_trait;
use eyre::Context;
use crate::{cli, cuddle_file::CuddleFile, MainAction};
#[derive(Clone)]
pub struct CuddleReleaser {
client: dagger_sdk::Query,
env: Option<String>,
cuddle_file: CuddleFile,
folder: String,
}
pub struct CuddleReleaserOptions {
upstream: String,
cluster: String,
namespace: String,
app: String,
}
pub enum CuddleEnv {
Prod,
Dev,
}
impl Display for CuddleEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CuddleEnv::Prod => f.write_str("prod"),
CuddleEnv::Dev => f.write_str("dev"),
}
}
}
impl TryInto<CuddleEnv> for String {
type Error = eyre::Error;
fn try_into(self) -> Result<CuddleEnv, Self::Error> {
let env = match self.as_str() {
"prod" => CuddleEnv::Prod,
"dev" => CuddleEnv::Dev,
_ => eyre::bail!("was not a valid env: {}", self),
};
Ok(env)
}
}
impl CuddleReleaser {
pub async fn new(client: dagger_sdk::Query) -> eyre::Result<Self> {
let cuddle_file = CuddleFile::from_cuddle_file().await?;
let env = std::env::var("CUDDLE_ENV").ok();
Ok(Self {
client,
cuddle_file,
env,
folder: ".cuddle/tmp".into(),
})
}
pub async fn releaser(&self, env: CuddleEnv) -> eyre::Result<()> {
let client = self.client.clone();
if self.cuddle_file.deployment.is_none() {
return Ok(());
}
let chosen_cluster = match self
.cuddle_file
.deployment
.as_ref()
.unwrap()
.env
.0
.get(&self.env.as_ref().unwrap_or(&env.to_string()).to_string())
{
Some(c) => match c.clusters.first().take() {
Some(c) => c,
None => return Ok(()),
},
None => return Ok(()),
};
if let Some(clusters) = &self.cuddle_file.vars.clusters {
let cluster = match clusters.0.get(chosen_cluster) {
Some(c) => c,
None => eyre::bail!("no cluster found for: {}", chosen_cluster),
};
let options = CuddleReleaserOptions {
cluster: chosen_cluster.clone(),
namespace: cluster.namespace.clone(),
app: self.cuddle_file.vars.service.clone(),
upstream: self
.cuddle_file
.deployment
.as_ref()
.unwrap()
.registry
.clone(),
};
let cuddle_releaser_image = "docker.io/kasperhermansen/cuddle-releaser:main-1706726858";
let folder = client.host().directory(&self.folder);
let ssh_sock = std::env::var("SSH_AUTH_SOCK").context("SSH_AUTH_SOCK not set")?;
let cuddle_releaser = client
.container()
.from(cuddle_releaser_image)
.with_env_variable("RUST_LOG", "trace")
.with_directory("/mnt/templates", folder)
.with_unix_socket(
ssh_sock.clone(),
client.host().unix_socket(ssh_sock.clone()),
);
let time = chrono::Local::now();
cuddle_releaser
.with_exec(vec!["echo", &time.to_rfc3339()])
.with_exec(vec![
"cuddle-releaser",
"release",
&format!("--upstream={}", options.upstream),
&format!("--folder={}", "/mnt/templates/k8s"),
&format!("--cluster={}", options.cluster),
&format!("--namespace={}", options.namespace),
&format!("--app={}", options.app),
])
.sync()
.await?;
}
Ok(())
}
}
#[async_trait]
impl MainAction for CuddleReleaser {
async fn execute_main(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.releaser(CuddleEnv::Prod).await?;
Ok(())
}
}

View File

@ -0,0 +1,68 @@
pub struct CuddleX {
command: String,
args: Vec<String>,
}
impl CuddleX {
pub fn command(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
}
}
pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.args.push(arg.into());
self
}
pub async fn run(&mut self) -> eyre::Result<(String, String, i32)> {
let mut cmd = tokio::process::Command::new("cuddle");
let cmd = cmd.arg("x").arg(&self.command).args(&self.args);
let process = cmd.spawn()?;
let output = process.wait_with_output().await?;
Ok((
std::str::from_utf8(&output.stdout)?.to_string(),
std::str::from_utf8(&output.stderr)?.to_string(),
output.status.code().unwrap_or(0),
))
}
}
pub mod well_known {
use super::CuddleX;
pub async fn render(args: impl IntoIterator<Item = impl Into<String>>) -> eyre::Result<()> {
tracing::info!("running render");
let mut cmd = CuddleX::command("render");
for arg in args.into_iter() {
let arg = arg.into();
cmd.arg(arg);
}
let (stdout, stderr, status) = cmd.run().await?;
for line in stdout.lines() {
tracing::trace!("render: {}", line);
}
for line in stderr.lines() {
tracing::trace!("render: {}", line);
}
if status != 0 {
tracing::warn!("finished render with non-zero exit code: {}", status);
}
tracing::info!("finished running render");
Ok(())
}
}

View File

@ -0,0 +1,54 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use std::{future::Future, pin::Pin, sync::Arc};
pub type DynMiddleware = Arc<dyn DaggerMiddleware + Send + Sync>;
#[async_trait]
pub trait DaggerMiddleware {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
Ok(container)
}
}
pub struct DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
pub func: F,
}
pub fn middleware<F>(func: F) -> Box<DaggerMiddlewareFn<F>>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
Box::new(DaggerMiddlewareFn { func })
}
#[async_trait]
impl<F> DaggerMiddleware for DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>> + Send + Sync,
{
async fn handle(&self, container: Container) -> eyre::Result<Container> {
// Call the closure stored in the struct
(self.func)(container).await
}
}
#[cfg(test)]
mod test {
use futures::FutureExt;
use super::*;
#[tokio::test]
async fn can_add_middleware() -> eyre::Result<()> {
middleware(|c| async move { Ok(c) }.boxed());
Ok(())
}
}

View File

@ -0,0 +1,87 @@
use std::{collections::BTreeMap, path::PathBuf, time::UNIX_EPOCH};
const DRONE_TEMPLATER_IMAGE: &str = "kasperhermansen/drone-templater:main-1711807810";
use async_trait::async_trait;
use eyre::{Context, OptionExt};
use crate::{rust_service::RustServiceContext, MainAction};
#[derive(Clone)]
pub struct DroneTemplater {
client: dagger_sdk::Query,
template: PathBuf,
variables: BTreeMap<String, String>,
}
impl DroneTemplater {
pub fn new(client: dagger_sdk::Query, template: impl Into<PathBuf>) -> Self {
Self {
client,
template: template.into(),
variables: BTreeMap::default(),
}
}
pub fn with_variable(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.variables.insert(name.into(), value.into());
self
}
}
#[async_trait]
impl MainAction for DroneTemplater {
async fn execute_main(&self, ctx: &mut crate::Context) -> eyre::Result<()> {
let image_tag = ctx
.get_image_tag()?
.ok_or_eyre(eyre::eyre!("failed to find image tag"))?;
let src = self.client.host().directory(".cuddle/tmp/");
let drone_host = std::env::var("DRONE_HOST").context("DRONE_HOST is missing")?;
let drone_user = std::env::var("DRONE_USER").context("DRONE_USER is missing")?;
let drone_token = std::env::var("DRONE_TOKEN").context("DRONE_TOKEN is missing")?;
let drone_token_secret = self.client.set_secret("DRONE_TOKEN", drone_token);
let now = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("failed to get system time")?;
let template_name = self.template.display().to_string();
let cmd = vec!["drone-templater", "upload", "--template", &template_name]
.into_iter()
.map(|v| v.to_string())
.chain(
self.variables
.iter()
.map(|(name, value)| format!(r#"--variable={name}={value}"#)),
)
.chain(vec![format!("--variable=image_tag={}", image_tag)])
.collect::<Vec<_>>();
self.client
.container()
.from(DRONE_TEMPLATER_IMAGE)
.with_env_variable("RUST_LOG", "drone_templater=trace")
.with_exec(vec!["echo", &format!("{}", now.as_secs())])
.with_directory("/src/templates", src)
.with_workdir("/src")
.with_env_variable("DRONE_HOST", drone_host)
.with_env_variable("DRONE_USER", drone_user)
.with_secret_variable("DRONE_TOKEN", drone_token_secret)
.with_exec(cmd)
.sync()
.await
.context("failed to upload drone templates with error")?;
Ok(())
}
}

View File

@ -0,0 +1,423 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use futures::{stream, StreamExt};
use crate::{
dagger_middleware::DaggerMiddleware,
rust_service::{
architecture::{Architecture, Os},
extensions::CargoBInstallExt,
RustServiceContext, RustServiceStage,
},
Context, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub struct LeptosService {
pub(crate) client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
final_image: Option<dagger_sdk::Container>,
stages: Vec<RustServiceStage>,
source: Option<PathBuf>,
crates: Vec<String>,
bin_name: String,
arch: Option<Architecture>,
os: Option<Os>,
deploy_target_name: Option<String>,
deploy: bool,
}
impl LeptosService {
pub fn new(client: dagger_sdk::Query, bin_name: impl Into<String>) -> Self {
Self {
client,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: bin_name.into(),
arch: None,
os: None,
deploy_target_name: None,
deploy: true,
}
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_deploy_target(&mut self, deploy_target: impl Into<String>) -> &mut Self {
self.deploy_target_name = Some(deploy_target.into());
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
pub fn with_deploy(&mut self, deploy: bool) -> &mut Self {
self.deploy = deploy;
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
pub fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
pub fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
async fn run_stage(
&self,
stages: impl IntoIterator<Item = &Arc<dyn DaggerMiddleware + Send + Sync>>,
container: Container,
) -> eyre::Result<Container> {
let before_deps_stream = stream::iter(stages.into_iter().map(Ok));
let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move {
match (base, m) {
(Ok(base), Ok(m)) => m.handle(base).await,
(_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"),
}
})
.await?;
Ok(res)
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let rust_src = RustSource::new(self.client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(self.client.container().from("rustlang/rust:nightly"));
let before_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeDeps(middleware) => Some(middleware),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_deps, base_image).await?;
let image = image.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"]);
let after_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterDeps(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_deps, image).await?;
let before_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_base, image).await?;
let cache = self.client.cache_volume("rust_target_cache");
let rust_prebuild = image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"-vv",
"--project",
&self.bin_name,
])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let after_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_base, rust_with_src).await?;
Ok(image)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
let binary_build = base.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"--project",
&self.bin_name,
]);
let after_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let binary_build = self.run_stage(after_build, binary_build).await?;
let dest = self
.final_image
.clone()
.unwrap_or(self.client.container().from("debian:bookworm"));
let before_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforePackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let dest = self.run_stage(before_package, dest).await?;
let final_image = dest
.with_workdir("/mnt/app")
.with_file(
format!("/usr/local/bin/{}", self.bin_name),
binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)),
)
.with_directory(
"/mnt/app/target/site",
binary_build.directory("/mnt/src/target/site"),
)
.with_file(
"/mnt/app/Cargo.toml",
binary_build.file(format!("/mnt/src/crates/{}/Cargo.toml", self.bin_name)),
)
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", &self.bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:3000")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(3000);
let after_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterPackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let final_image = self.run_stage(after_package, final_image).await?;
Ok(final_image)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
base.with_exec(vec!["cargo", "leptos", "test", "--release"])
.sync()
.await?;
Ok(())
}
fn get_deploy_target(&self) -> String {
self.deploy_target_name
.clone()
.unwrap_or(self.bin_name.clone())
}
}
#[async_trait]
impl PullRequestAction for LeptosService {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
let mut s = self.clone();
s.with_cargo_binstall("latest", ["cargo-leptos"])
.build_test()
.await?;
Ok(())
}
}
#[async_trait]
impl MainAction for LeptosService {
async fn execute_main(&self, ctx: &mut Context) -> eyre::Result<()> {
let mut s = self.clone();
let container = s
.with_cargo_binstall("latest", ["cargo-leptos"])
.build_release()
.await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:main-{}",
self.bin_name, timestamp,
);
container.publish(tag.clone()).await?;
tracing::info!("published: {}", tag);
ctx.set_image_tag(format!("main-{}", &timestamp.to_string()))?;
if self.deploy {
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
dep.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.get_deploy_target()
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
} else {
dep.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.get_deploy_target()
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
};
dep.sync().await?;
}
Ok(())
}
}

View File

@ -0,0 +1,15 @@
pub mod cli;
pub use cli::*;
pub mod leptos_service;
pub mod node_service;
pub mod rust_lib;
pub mod rust_service;
pub mod cuddle_file;
pub mod cuddle_please;
pub mod cuddle_releaser;
pub mod cuddle_x;
pub mod dagger_middleware;
pub mod drone_templater;
pub mod rust_workspace;

View File

@ -0,0 +1,290 @@
use std::path::PathBuf;
use async_trait::async_trait;
use dagger_sdk::{Container, ContainerWithDirectoryOptsBuilder, HostDirectoryOptsBuilder};
use crate::{
dagger_middleware::DynMiddleware,
rust_service::architecture::{Architecture, Os},
Context, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub enum NodeServiceStage {
BeforeDeps(DynMiddleware),
AfterDeps(DynMiddleware),
BeforeBase(DynMiddleware),
AfterBase(DynMiddleware),
BeforeBuild(DynMiddleware),
AfterBuild(DynMiddleware),
BeforePackage(DynMiddleware),
AfterPackage(DynMiddleware),
BeforeRelease(DynMiddleware),
AfterRelease(DynMiddleware),
}
#[derive(Clone)]
pub struct NodeService {
client: dagger_sdk::Query,
service: String,
base_image: Option<Container>,
final_image: Option<Container>,
stages: Vec<NodeServiceStage>,
source: Option<PathBuf>,
arch: Option<Architecture>,
os: Option<Os>,
}
impl NodeService {
pub fn new(value: dagger_sdk::Query, service: impl Into<String>) -> Self {
Self {
client: value,
service: service.into(),
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
arch: None,
os: None,
}
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_service(&mut self, service: impl Into<String>) -> &mut Self {
self.service = service.into();
self
}
pub fn with_stage(&mut self, stage: NodeServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let src = self.client.host().directory_opts(
self.get_src().to_string_lossy(),
HostDirectoryOptsBuilder::default()
.exclude(vec!["node_modules/", ".git/", ".cuddle/"])
.build()?,
);
let pkg_files = self.client.host().directory_opts(
self.get_src().to_string_lossy(),
HostDirectoryOptsBuilder::default()
.include(vec!["package.json", "yarn.lock"])
.build()?,
);
let deps =
"apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git postgresql-dev"
.split_whitespace();
let base_image = match self.base_image.clone() {
Some(image) => image,
None => self
.client
.container()
.from("node:20-alpine")
.with_exec(vec!["apk", "update"])
.with_exec(deps.collect()),
}
.with_env_variable("NODE_ENV", "production");
let base_yarn_image = base_image
.with_workdir("/opt/")
.with_directory(".", pkg_files)
.with_exec(vec!["yarn", "global", "add", "node-gyp"])
.with_exec(vec![
"yarn",
"config",
"set",
"network-timeout",
"600000",
"-g",
])
.with_exec(vec!["yarn", "install", "--production"]);
let base_build = base_yarn_image
.with_env_variable(
"PATH",
format!(
"/opt/node_modules/.bin:{}",
base_yarn_image.env_variable("PATH").await?
),
)
.with_workdir("/opt/app")
.with_directory(".", src)
.with_exec(vec!["yarn", "build"]);
Ok(base_build)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let final_build_image = match self.final_image.clone() {
Some(c) => c,
None => self
.client
.container()
.from("node:20-alpine")
.with_exec(vec![
"apk",
"add",
"--no-cache",
"vips-dev",
"postgresql-dev",
]),
}
.with_env_variable("NODE_ENV", "production");
let final_image = final_build_image
.with_workdir("/opt/")
.with_directory("/opt/node_modules", base.directory("/opt/node_modules"))
.with_workdir("/opt/app")
.with_directory_opts(
"/opt/app",
base.directory("/opt/app"),
ContainerWithDirectoryOptsBuilder::default()
.owner("node:node")
.build()?,
)
.with_env_variable(
"PATH",
format!(
"/opt/node_modules/.bin:{}",
final_build_image.env_variable("PATH").await?
),
)
.with_user("node")
.with_exposed_port(1337)
.with_entrypoint(vec!["yarn", "start"]);
Ok(final_image)
}
}
#[async_trait]
impl PullRequestAction for NodeService {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
let release = self.build_release().await?;
release.sync().await?;
Ok(())
}
}
#[async_trait]
impl MainAction for NodeService {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
container
.publish(format!(
"docker.io/kasperhermansen/{}:main-{}",
self.service, timestamp,
))
.await?;
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = match std::env::var("SSH_AUTH_SOCK").ok() {
Some(sock) => dep
.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.service
),
"--service",
&self.service,
"--image",
&format!("kasperhermansen/{}:main-{}", self.service, timestamp),
]),
_ => dep
.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.service
),
"--service",
&self.service,
"--image",
&format!("kasperhermansen/{}:main-{}", self.service, timestamp),
]),
};
dep.sync().await?;
Ok(())
}
}

View File

@ -0,0 +1,147 @@
use std::path::PathBuf;
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use crate::{
cli,
rust_service::architecture::{Architecture, Os},
rust_workspace, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub struct RustLib {
client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
source: Option<PathBuf>,
crates: Vec<String>,
arch: Option<Architecture>,
os: Option<Os>,
}
impl RustLib {
pub fn new(value: dagger_sdk::Query) -> Self {
Self {
client: value,
source: None,
crates: Vec::new(),
arch: None,
os: None,
base_image: None,
}
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub async fn with_workspace_crates(&mut self) -> &mut Self {
if let Ok(Some(file)) = rust_workspace::File::read_file().await {
if let Some(members) = file.get_workspace_members() {
return self.with_crates(members);
}
}
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let rust_src = RustSource::new(self.client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(self.client.container().from("rustlang/rust:nightly"));
let cache = self.client.cache_volume("rust_target_cache");
let rust_prebuild = base_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec!["cargo", "build", "--tests", "--workspace"])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = base_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
base.with_exec(vec!["cargo", "test", "--tests", "--workspace"])
.sync()
.await?;
Ok(())
}
}
#[async_trait]
impl PullRequestAction for RustLib {
async fn execute_pull_request(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.build_test().await?;
Ok(())
}
}
#[async_trait]
impl MainAction for RustLib {
async fn execute_main(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.build_test().await?;
Ok(())
}
}

View File

@ -0,0 +1,527 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use futures::{stream, StreamExt};
use crate::{
dagger_middleware::{DaggerMiddleware, DynMiddleware},
Context, MainAction, PullRequestAction,
};
use self::architecture::{Architecture, Os};
#[derive(Clone)]
pub enum RustServiceStage {
BeforeDeps(DynMiddleware),
AfterDeps(DynMiddleware),
BeforeBase(DynMiddleware),
AfterBase(DynMiddleware),
BeforeBuild(DynMiddleware),
AfterBuild(DynMiddleware),
BeforePackage(DynMiddleware),
AfterPackage(DynMiddleware),
BeforeRelease(DynMiddleware),
AfterRelease(DynMiddleware),
}
#[derive(Clone)]
pub struct RustService {
client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
final_image: Option<dagger_sdk::Container>,
stages: Vec<RustServiceStage>,
source: Option<PathBuf>,
crates: Vec<String>,
bin_name: String,
arch: Option<Architecture>,
os: Option<Os>,
deployment: bool,
}
impl From<dagger_sdk::Query> for RustService {
fn from(value: dagger_sdk::Query) -> Self {
Self {
client: value,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
arch: None,
os: None,
deployment: true,
}
}
}
impl RustService {
pub async fn new(client: dagger_sdk::Query) -> eyre::Result<Self> {
Ok(Self {
client,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
arch: None,
os: None,
deployment: true,
})
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_bin_name(&mut self, bin_name: impl Into<String>) -> &mut Self {
self.bin_name = bin_name.into();
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
pub fn with_deployment(&mut self, deployment: bool) -> &mut Self {
self.deployment = deployment;
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
async fn run_stage(
&self,
stages: impl IntoIterator<Item = &Arc<dyn DaggerMiddleware + Send + Sync>>,
container: Container,
) -> eyre::Result<Container> {
let before_deps_stream = stream::iter(stages.into_iter().map(Ok));
let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move {
match (base, m) {
(Ok(base), Ok(m)) => m.handle(base).await,
(_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"),
}
})
.await?;
Ok(res)
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let client = self.client.clone();
let rust_src = RustSource::new(client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(client.container().from("rustlang/rust:nightly"));
let before_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeDeps(middleware) => Some(middleware),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_deps, base_image).await?;
let after_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterDeps(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_deps, image).await?;
let before_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_base, image).await?;
let cache = client.cache_volume("rust_target_cache");
let rust_prebuild = image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let after_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_base, rust_with_src).await?;
Ok(image)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let client = self.client.clone();
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
let binary_build =
base.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name]);
let after_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let binary_build = self.run_stage(after_build, binary_build).await?;
let dest = self
.final_image
.clone()
.unwrap_or(client.container().from("debian:bookworm"));
let before_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforePackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let dest = self.run_stage(before_package, dest).await?;
let final_image = dest.with_workdir("/mnt/app").with_file(
format!("/usr/local/bin/{}", self.bin_name),
binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)),
);
let after_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterPackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let final_image = self.run_stage(after_package, final_image).await?;
Ok(final_image)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
base.with_exec(vec!["cargo", "test", "--release"])
.sync()
.await?;
Ok(())
}
}
#[async_trait]
impl PullRequestAction for RustService {
async fn execute_pull_request(&self, ctx: &mut Context) -> eyre::Result<()> {
self.build_test().await?;
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:dev-{}",
self.bin_name, timestamp,
);
container.publish(&tag).await?;
ctx.set_image_tag(format!("dev-{}", &timestamp.to_string()))?;
Ok(())
}
}
const IMAGE_TAG: &str = "RUST_SERVICE_IMAGE_TAG";
pub trait RustServiceContext {
fn set_image_tag(&mut self, tag: impl Into<String>) -> eyre::Result<()>;
fn get_image_tag(&self) -> eyre::Result<Option<String>>;
}
impl RustServiceContext for Context {
fn get_image_tag(&self) -> eyre::Result<Option<String>> {
Ok(self.get(IMAGE_TAG).cloned())
}
fn set_image_tag(&mut self, tag: impl Into<String>) -> eyre::Result<()> {
let tag = tag.into();
self.insert(IMAGE_TAG.to_string(), tag);
Ok(())
}
}
#[async_trait]
impl MainAction for RustService {
async fn execute_main(&self, ctx: &mut Context) -> eyre::Result<()> {
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:main-{}",
self.bin_name, timestamp,
);
container.publish(&tag).await?;
ctx.set_image_tag(format!("main-{}", &timestamp.to_string()))?;
if self.deployment {
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
dep.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.bin_name
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
} else {
dep.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.bin_name
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
};
dep.sync().await?;
}
Ok(())
}
}
pub mod architecture {
#[derive(Debug, Clone)]
pub enum Architecture {
Amd64,
Arm64,
}
#[derive(Debug, Clone)]
pub enum Os {
Linux,
MacOS,
}
}
mod apt;
mod apt_ca_certificates;
mod assets;
mod cargo_binstall;
mod cargo_clean;
mod clap_sanity_test;
mod cuddle_cli;
mod cuddle_file;
mod dagger_bin;
mod docker_cache;
mod docker_cli;
mod kubectl;
mod mold;
mod rust_workspace;
mod sqlx;
mod ssh_agent;
pub mod extensions {
pub use super::apt::*;
pub use super::apt_ca_certificates::*;
pub use super::assets::*;
pub use super::cargo_binstall::*;
pub use super::cargo_clean::*;
pub use super::clap_sanity_test::*;
pub use super::cuddle_cli::*;
pub use super::cuddle_file::*;
pub use super::dagger_bin::*;
pub use super::docker_cache::*;
pub use super::docker_cli::*;
pub use super::kubectl::*;
pub use super::mold::*;
pub use super::rust_workspace::*;
pub use super::sqlx::*;
pub use super::ssh_agent::*;
}
#[cfg(test)]
mod test {
#[tokio::test]
#[cfg(any(feature = "dagger", feature = "integration"))]
async fn test_can_build_rust() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let root_dir = std::path::PathBuf::from("../../").canonicalize()?;
let container = RustService::from(client.clone())
.with_arch(Architecture::Amd64)
.with_os(Os::Linux)
.with_source(root_dir)
.with_bin_name("ci")
.with_crates(["crates/*", "examples/*", "ci"])
.with_apt(&["git"])
.with_cargo_binstall("latest", ["sqlx-cli"])
.with_mold("2.3.3")
// .with_stage(RustServiceStage::BeforeDeps(middleware(|c| {
// async move {
// // Noop
// Ok(c)
// }
// .boxed()
// })))
.with_clap_sanity_test()
.build_release()
.await?;
container.sync().await?;
Ok(())
})
.await?;
Ok(())
}
}

View File

@ -0,0 +1,86 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct Apt {
deps: Vec<String>,
}
impl Default for Apt {
fn default() -> Self {
Self::new()
}
}
impl Apt {
pub fn new() -> Self {
Self { deps: Vec::new() }
}
pub fn add(mut self, dep_name: impl Into<String>) -> Self {
self.deps.push(dep_name.into());
self
}
pub fn extend(mut self, deps: &[&str]) -> Self {
self.deps.extend(deps.iter().map(|s| s.to_string()));
self
}
}
#[async_trait]
impl DaggerMiddleware for Apt {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let mut deps = vec!["apt", "install", "-y"];
deps.extend(self.deps.iter().map(|s| s.as_str()));
let c = container.with_exec(vec!["apt", "update"]).with_exec(deps);
Ok(c)
}
}
pub trait AptExt {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self
}
}
impl AptExt for RustService {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
Apt::new().extend(deps),
)))
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Apt::new().extend(deps),
)))
}
}
impl AptExt for LeptosService {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
Apt::new().extend(deps),
)))
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Apt::new().extend(deps),
)))
}
}

View File

@ -0,0 +1,62 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct AptCaCertificates {}
impl Default for AptCaCertificates {
fn default() -> Self {
Self::new()
}
}
impl AptCaCertificates {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for AptCaCertificates {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let c = container
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "ca-certificates"])
.with_exec(vec!["update-ca-certificates"]);
Ok(c)
}
}
pub trait AptCaCertificatesExt {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self
}
}
impl AptCaCertificatesExt for RustService {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
AptCaCertificates::new(),
)))
.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
AptCaCertificates::new(),
)))
}
}
impl AptCaCertificatesExt for LeptosService {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
AptCaCertificates::new(),
)))
.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
AptCaCertificates::new(),
)))
}
}

View File

@ -0,0 +1,68 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct Assets {
client: dagger_sdk::Query,
assets: Vec<(PathBuf, PathBuf)>,
}
impl Assets {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
assets: Vec::default(),
}
}
fn with_folders(mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> Self {
let mut folders = folders.into_iter().collect::<Vec<(PathBuf, PathBuf)>>();
self.assets.append(&mut folders);
self
}
}
#[async_trait]
impl DaggerMiddleware for Assets {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let container =
self.assets
.iter()
.fold(container, |container, (src_asset_path, dest_asset_path)| {
let src_path = src_asset_path.display().to_string();
let dest_path = dest_asset_path.display().to_string();
let path = self.client.host().directory(src_path);
container.with_directory(dest_path, path)
});
Ok(container)
}
}
pub trait AssetsExt {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self
}
}
impl AssetsExt for RustService {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
Assets::new(self.client.clone()).with_folders(folders),
)))
}
}
impl AssetsExt for LeptosService {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
Assets::new(self.client.clone()).with_folders(folders),
)))
}
}

View File

@ -0,0 +1,120 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct CargoBInstall {
arch: Architecture,
os: Os,
version: String,
crates: Vec<String>,
}
impl CargoBInstall {
pub fn new(
arch: Architecture,
os: Os,
version: impl Into<String>,
crates: impl Into<Vec<String>>,
) -> Self {
Self {
arch,
os,
version: version.into(),
crates: crates.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "armv7",
}
.into()
}
fn get_os(&self) -> String {
match self.os {
Os::Linux => "linux",
Os::MacOS => "darwin",
}
.into()
}
pub fn get_download_url(&self) -> String {
format!("https://github.com/cargo-bins/cargo-binstall/releases/{}/download/cargo-binstall-{}-unknown-{}-musl.tgz", self.version, self.get_arch(), self.get_os())
}
pub fn get_archive(&self) -> String {
format!(
"cargo-binstall-{}-unknown-{}-musl.tgz",
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for CargoBInstall {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let c = container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive()])
.with_exec(
"mv cargo-binstall /usr/local/cargo/bin"
.split_whitespace()
.collect(),
);
let c = self.crates.iter().cloned().fold(c, |acc, item| {
acc.with_exec(vec!["cargo", "binstall", &item, "-y"])
});
Ok(c)
}
}
pub trait CargoBInstallExt {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self
}
}
impl CargoBInstallExt for RustService {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
let crates: Vec<String> = crates.into_iter().map(|s| s.into()).collect();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
CargoBInstall::new(self.get_arch(), self.get_os(), version, crates),
)))
}
}
impl CargoBInstallExt for LeptosService {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
let crates: Vec<String> = crates.into_iter().map(|s| s.into()).collect();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
CargoBInstall::new(self.get_arch(), self.get_os(), version, crates),
)))
}
}

View File

@ -0,0 +1,45 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct CargoClean;
impl Default for CargoClean {
fn default() -> Self {
Self::new()
}
}
impl CargoClean {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for CargoClean {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_exec(vec!["cargo", "clean"]))
}
}
pub trait CargoCleanExt {
fn with_cargo_clean(&mut self) -> &mut Self {
self
}
}
impl CargoCleanExt for RustService {
fn with_cargo_clean(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(
CargoClean::new(),
)));
self
}
}

View File

@ -0,0 +1,43 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct ClapSanityTest {
bin_name: String,
}
impl ClapSanityTest {
pub fn new(bin_name: impl Into<String>) -> Self {
Self {
bin_name: bin_name.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for ClapSanityTest {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_exec(vec![&self.bin_name, "--help"]))
}
}
pub trait ClapSanityTestExt {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self
}
}
impl ClapSanityTestExt for RustService {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
ClapSanityTest::new(&self.bin_name),
)));
self
}
}

View File

@ -0,0 +1,49 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct CuddleCli {
client: dagger_sdk::Query,
}
impl CuddleCli {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for CuddleCli {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let cuddle = self
.client
.container()
.from("kasperhermansen/cuddle:latest");
Ok(container.with_file(
"/usr/local/bin/cuddle",
cuddle.file("/usr/local/cargo/bin/cuddle"),
))
}
}
pub trait CuddleCliExt {
fn with_cuddle_cli(&mut self) -> &mut Self {
self
}
}
impl CuddleCliExt for RustService {
fn with_cuddle_cli(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
CuddleCli::new(self.client.clone()),
)));
self
}
}

View File

@ -0,0 +1,86 @@
use std::path::PathBuf;
use crate::{
cuddle_file::{CuddleDatabase, CuddleFile},
rust_service::extensions::AptExt,
};
use super::{
extensions::{AssetsExt, CargoCleanExt, SqlxExt},
RustService,
};
#[derive(Default)]
pub struct CuddleFileAction {}
impl CuddleFileAction {
pub fn new() -> Self {
Self {}
}
}
pub trait CuddleFileExt {
fn with_cuddle_file(&mut self, cuddle_file: &CuddleFile) -> &mut Self;
}
impl CuddleFileExt for RustService {
fn with_cuddle_file(&mut self, cuddle_file: &CuddleFile) -> &mut Self {
let mut s = self
.with_bin_name(&cuddle_file.vars.service)
.with_deployment(false);
tracing::trace!("with cuddle file: {:+?}", &cuddle_file);
if let Some(components) = &cuddle_file.components {
s = if let Some(database) = &components.database {
match database {
CuddleDatabase::Enabled(true) => s.with_sqlx_migrations(
PathBuf::from("crates")
.join(&cuddle_file.vars.service)
.join("migrations/crdb"),
),
CuddleDatabase::Values { migrations } => s.with_sqlx_migrations(migrations),
CuddleDatabase::Enabled(false) | CuddleDatabase::Default {} => s,
}
} else {
s
};
if let Some(assets) = &components.assets {
if let Some(true) = assets.clean {
s = s.with_cargo_clean()
}
if let Some(volumes) = &assets.volumes {
let mappings = volumes.iter().cloned().map(|val| (val.from, val.to));
s = s.with_assets(mappings);
}
}
if let Some(packages) = &components.packages {
s = s
.with_apt(
packages
.debian
.dev
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.as_slice(),
)
.with_apt_release(
packages
.debian
.release
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.as_slice(),
);
}
}
s
}
}

View File

@ -0,0 +1,72 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DaggerBin {
client: dagger_sdk::Query,
version: String,
}
impl DaggerBin {
pub fn new(client: dagger_sdk::Query, version: impl Into<String>) -> Self {
Self {
client,
version: version.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for DaggerBin {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let install_script = self.client.http("https://dl.dagger.io/dagger/install.sh");
let dagger_bin = self
.client
.container()
.from("alpine")
.with_file_opts(
"/mnt/install.sh",
install_script,
dagger_sdk::ContainerWithFileOpts {
owner: None,
permissions: Some(0o755),
expand: None,
},
)
.with_env_variable("DAGGER_VERSION", &self.version)
.with_exec(vec!["/mnt/install.sh"])
.file("/bin/dagger");
Ok(container
.with_file_opts(
"/usr/local/bin/dagger",
dagger_bin,
dagger_sdk::ContainerWithFileOpts {
owner: None,
permissions: Some(0o755),
expand: None,
},
)
.with_exec(vec!["/usr/local/bin/dagger", "version"]))
}
}
pub trait DaggerBinExt {
fn with_dagger_bin(&mut self, dagger_version: impl Into<String>) -> &mut Self;
}
impl DaggerBinExt for RustService {
fn with_dagger_bin(&mut self, dagger_version: impl Into<String>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
DaggerBin::new(self.client.clone(), dagger_version.into()),
)));
self
}
}

View File

@ -0,0 +1,70 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container, ImageMediaTypes};
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DockerCache {
client: dagger_sdk::Query,
image_name: String,
}
impl DockerCache {
pub fn new(client: dagger_sdk::Query, image_name: impl Into<String>) -> Self {
Self {
client,
image_name: image_name.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for DockerCache {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
match (
std::env::var("REGISTRY_CACHE_USERNAME"),
std::env::var("REGISTRY_CACHE_PASSWORD"),
) {
(Ok(username), Ok(password)) => {
let url = format!("harbor.front.kjuulh.io/cache/{}:cache", self.image_name);
let secret = self.client.set_secret("REGISTRY_CACHE_PASSWORD", password);
container
.with_registry_auth(&url, &username, secret)
.publish_opts(
&url,
dagger_sdk::ContainerPublishOpts {
forced_compression: Some(dagger_sdk::ImageLayerCompression::Zstd),
media_types: Some(ImageMediaTypes::OciMediaTypes),
platform_variants: None,
},
)
.await?;
}
_ => {
eprintln!("failed to find REGISTRY_CACHE_USERNAME or REGISTRY_CACHE_PASSWORD");
}
}
Ok(container)
}
}
pub trait DockerCacheExt {
fn with_docker_cache(&mut self) -> &mut Self {
self
}
}
impl DockerCacheExt for RustService {
fn with_docker_cache(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
DockerCache::new(self.client.clone(), self.bin_name.clone()),
)));
self
}
}

View File

@ -0,0 +1,48 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container};
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DockerCli {
client: dagger_sdk::Query,
}
impl DockerCli {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for DockerCli {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let docker = self.client.container().from("docker:cli");
Ok(container
.with_file(
"/usr/local/bin/docker",
docker.file("/usr/local/bin/docker"),
)
.with_directory("/certs", docker.directory("/certs")))
}
}
pub trait DockerCliExt {
fn with_docker_cli(&mut self) -> &mut Self {
self
}
}
impl DockerCliExt for RustService {
fn with_docker_cli(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
DockerCli::new(self.client.clone()),
)));
self
}
}

View File

@ -0,0 +1,75 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct Kubectl {
client: dagger_sdk::Query,
}
impl Kubectl {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
const KUBESLICEDOWNLOAD: &str = r#"slice_VERSION=v1.2.7 && \
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"#;
#[async_trait]
impl DaggerMiddleware for Kubectl {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let kubectl = self
.client
.container()
.from("line/kubectl-kustomize:1.29.1-5.3.0");
let kubeslice = self
.client
.container()
.from("alpine:3.19")
.with_exec(vec!["apk", "add", "tar", "wget"])
.with_exec(vec!["sh", "-c", KUBESLICEDOWNLOAD]);
let helm = self.client.container().from("alpine/helm:3.11.1");
Ok(container
.with_file(
"/usr/local/bin/kubectl",
kubectl.file("/usr/local/bin/kubectl"),
)
.with_file(
"/usr/local/bin/kustomize",
kubectl.file("/usr/local/bin/kustomize"),
)
.with_file(
"/usr/local/bin/kubectl-slice",
kubeslice.file("/usr/local/bin/kubectl-slice"),
)
.with_file("/usr/local/bin/helm", helm.file("/usr/bin/helm")))
}
}
pub trait KubectlExt {
fn with_kubectl(&mut self) -> &mut Self {
self
}
}
impl KubectlExt for RustService {
fn with_kubectl(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Kubectl::new(self.client.clone()),
)));
self
}
}

View File

@ -0,0 +1,104 @@
use std::sync::Arc;
use async_trait::async_trait;
use crate::dagger_middleware::DaggerMiddleware;
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct MoldInstall {
arch: Architecture,
os: Os,
version: String,
}
impl MoldInstall {
pub fn new(arch: Architecture, os: Os, version: impl Into<String>) -> Self {
Self {
arch,
os,
version: version.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "arm",
}
.into()
}
fn get_os(&self) -> String {
match &self.os {
Os::Linux => "linux",
o => todo!("os not implemented for mold: {:?}", o),
}
.into()
}
pub fn get_download_url(&self) -> String {
format!(
"https://github.com/rui314/mold/releases/download/v{}/mold-{}-{}-{}.tar.gz",
self.version,
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_folder(&self) -> String {
format!(
"mold-{}-{}-{}",
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_archive_name(&self) -> String {
format!(
"mold-{}-{}-{}.tar.gz",
self.version,
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for MoldInstall {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
println!("installing mold");
let c = container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive_name()])
.with_exec(vec![
"mv",
&format!("{}/bin/mold", self.get_folder()),
"/usr/bin/mold",
]);
Ok(c)
}
}
pub trait MoldActionExt {
fn with_mold(&mut self, version: impl Into<String>) -> &mut Self {
self
}
}
impl MoldActionExt for RustService {
fn with_mold(&mut self, version: impl Into<String>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterDeps(Arc::new(
MoldInstall::new(self.get_arch(), self.get_os(), version),
)))
}
}

View File

@ -0,0 +1,66 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService, rust_workspace};
use super::RustService;
pub struct RustWorkspace {}
impl Default for RustWorkspace {
fn default() -> Self {
Self::new()
}
}
impl RustWorkspace {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for RustWorkspace {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container)
}
}
#[async_trait]
pub trait RustWorkspaceExt {
async fn with_workspace_crates(&mut self) -> &mut Self {
self
}
}
#[async_trait]
impl RustWorkspaceExt for RustService {
async fn with_workspace_crates(&mut self) -> &mut Self {
self.with_crates(get_members().await)
.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
RustWorkspace::new(),
)))
}
}
#[async_trait]
impl RustWorkspaceExt for LeptosService {
async fn with_workspace_crates(&mut self) -> &mut Self {
self.with_crates(get_members().await)
.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
RustWorkspace::new(),
)))
}
}
async fn get_members() -> Vec<String> {
if let Ok(Some(file)) = rust_workspace::File::read_file().await {
if let Some(members) = file.get_workspace_members() {
return members;
}
}
Vec::new()
}

View File

@ -0,0 +1,81 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct Sqlx {
client: dagger_sdk::Query,
migration_path: Option<PathBuf>,
}
impl Sqlx {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
migration_path: None,
}
}
pub fn with_migration_path(mut self, migration_path: impl Into<PathBuf>) -> Self {
self.migration_path = Some(migration_path.into());
self
}
}
#[async_trait]
impl DaggerMiddleware for Sqlx {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let container = if std::path::PathBuf::from(".sqlx/").exists() {
tracing::debug!("found .sqlx folder enabling offline mode");
let src = self.client.host().directory(".sqlx/");
container
.with_directory(".sqlx", src)
.with_env_variable("SQLX_OFFLINE", "true")
} else {
tracing::debug!("did not find a .sqlx folder, requires a running database");
container
};
let container = if let Some(migration_path) = &self.migration_path {
container
.with_directory(
"/mnt/sqlx/migrations",
self.client
.host()
.directory(migration_path.display().to_string()),
)
.with_env_variable("NEFARIOUS_DB_MIGRATION_PATH", "/mnt/sqlx/migrations")
} else {
container
};
Ok(container)
}
}
pub trait SqlxExt {
fn with_sqlx(&mut self) -> &mut Self;
fn with_sqlx_migrations(&mut self, path: impl Into<PathBuf>) -> &mut Self;
}
impl SqlxExt for RustService {
fn with_sqlx(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(Sqlx::new(
self.client.clone(),
))))
}
fn with_sqlx_migrations(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(
Sqlx::new(self.client.clone()).with_migration_path(path),
)))
}
}

View File

@ -0,0 +1,79 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container, ContainerWithNewFileOptsBuilder};
use eyre::Context;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct SshAgent {
client: dagger_sdk::Query,
}
impl SshAgent {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for SshAgent {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let sock_var =
std::env::var("SSH_AUTH_SOCK").context("failed to find variable SSH_AUTH_SOCK")?;
let socket = self.client.host().unix_socket(&sock_var);
let c = container
.with_new_file_opts(
".ssh/config".to_string(),
r#"
Host *
UserKnownHostsFile=/dev/null
StrictHostKeyChecking no
"#,
ContainerWithNewFileOptsBuilder::default()
.permissions(0o700_isize)
.build()?,
)
.with_unix_socket(&sock_var, socket)
.with_env_variable("SSH_AUTH_SOCK", &sock_var);
Ok(c)
}
}
pub trait SshAgentExt {
fn with_ssh_agent(&mut self) -> &mut Self {
self
}
}
impl SshAgentExt for RustService {
fn with_ssh_agent(&mut self) -> &mut Self {
let client = self.client.clone();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
SshAgent::new(client.clone()),
)))
.with_stage(super::RustServiceStage::BeforeBase(Arc::new(
SshAgent::new(client),
)))
}
}
impl SshAgentExt for LeptosService {
fn with_ssh_agent(&mut self) -> &mut Self {
let client = self.client.clone();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
SshAgent::new(client.clone()),
)))
.with_stage(super::RustServiceStage::AfterBase(Arc::new(SshAgent::new(
client,
))))
}
}

View File

@ -0,0 +1,31 @@
use serde::Deserialize;
#[derive(Deserialize, Clone, Debug)]
pub struct Workspace {
pub members: Vec<String>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct File {
pub workspace: Option<Workspace>,
}
impl File {
pub async fn read_file() -> eyre::Result<Option<Self>> {
let file = match tokio::fs::read_to_string("Cargo.toml").await {
Ok(file) => file,
Err(e) => {
tracing::warn!("Cargo.toml was not found: {}", e);
return Ok(None);
}
};
let workspace_file: File = toml::from_str(&file)?;
Ok(Some(workspace_file))
}
pub fn get_workspace_members(&self) -> Option<Vec<String>> {
self.workspace.as_ref().map(|w| w.members.clone())
}
}

View File

@ -1,11 +1,16 @@
[package]
name = "dagger-cuddle-please"
version = "0.1.0"
edition = "2021"
description = "A set of components for running cuddle-please in dagger"
version.workspace = true
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait = "*"
async-trait.workspace = true

View File

@ -80,11 +80,11 @@ pub mod traits {
}
}
pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease + Send + Sync + 'static>);
pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease>);
impl DaggerCuddlePleaseAction {
/// Create a [`traits::CuddlePlease`] client based on dagger
pub fn dagger(client: Arc<dagger_sdk::Query>) -> Self {
pub fn dagger(client: dagger_sdk::Query) -> Self {
Self(Arc::new(DaggerCuddlePlease::new(client)))
}
@ -103,32 +103,28 @@ impl DaggerCuddlePleaseAction {
}
#[derive(Clone)]
struct DaggerCuddlePlease {
client: Arc<dagger_sdk::Query>,
pub struct DaggerCuddlePlease {
client: dagger_sdk::Query,
}
#[async_trait::async_trait]
impl CuddlePlease for DaggerCuddlePlease {
async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
self.cuddle_please(self.client.clone(), args).await
self.cuddle_please(args).await
}
async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
self.cuddle_please_src(self.client.clone(), args).await
self.cuddle_please_src(args).await
}
}
impl DaggerCuddlePlease {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
pub async fn cuddle_please(
&self,
client: Arc<dagger_sdk::Query>,
args: &CuddlePleaseArgs,
) -> eyre::Result<()> {
let build_image = client.container().from(&args.cuddle_image);
pub async fn cuddle_please(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
let build_image = self.client.container().from(&args.cuddle_image);
let repo_url = match &args.server {
Server::Gitea {
@ -182,30 +178,32 @@ impl DaggerCuddlePlease {
};
let src = if args.use_ssh_socket {
let socket = client
let socket = self
.client
.host()
.unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set"));
client
self.client
.git_opts(
&repo_url,
dagger_sdk::QueryGitOpts {
experimental_service_host: None,
keep_git_dir: Some(true),
ssh_auth_socket: Some(socket.id().await?),
ssh_known_hosts: None,
},
)
.branch("main")
.tree_opts(dagger_sdk::GitRefTreeOpts {
ssh_auth_socket: Some(socket.id().await?),
ssh_known_hosts: None,
})
.tree()
} else {
client
self.client
.git_opts(
&repo_url,
dagger_sdk::QueryGitOpts {
experimental_service_host: None,
keep_git_dir: Some(true),
ssh_auth_socket: None,
ssh_known_hosts: None,
},
)
.branch("main")
@ -215,19 +213,16 @@ impl DaggerCuddlePlease {
let res = build_image
.with_secret_variable(
"CUDDLE_PLEASE_TOKEN",
client
.set_secret(
self.client.set_secret(
"CUDDLE_PLEASE_TOKEN",
match &args.server {
Server::Gitea { token, .. } => token,
Server::GitHub { token } => token,
},
)
.id()
.await?,
),
)
.with_workdir("/mnt/app")
.with_directory(".", src.id().await?)
.with_directory(".", src)
.with_exec(vec!["git", "remote", "set-url", "origin", &repo_url])
.with_exec(vec![
"cuddle-please",
@ -257,10 +252,7 @@ impl DaggerCuddlePlease {
},
]);
let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
res.sync().await?;
let please_out = res.stdout().await?;
println!("{please_out}");
@ -269,55 +261,63 @@ impl DaggerCuddlePlease {
Ok(())
}
pub async fn cuddle_please_src(
&self,
client: Arc<dagger_sdk::Query>,
args: &CuddlePleaseSrcArgs,
) -> eyre::Result<()> {
let build_image = client.container().from(&args.cuddle_image);
pub async fn cuddle_please_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
let build_image = self.client.container().from(&args.cuddle_image);
let res = build_image
.with_secret_variable(
"CUDDLE_PLEASE_TOKEN",
client
.set_secret(
self.client.set_secret(
"CUDDLE_PLEASE_TOKEN",
match &args.server {
SrcServer::Gitea { token, .. } => token,
SrcServer::GitHub { token } => token,
},
)
.id()
.await?,
),
)
.with_workdir("/mnt/app")
.with_directory(".", client.host().directory(".").id().await?)
.with_directory(".", self.client.host().directory("."))
.with_unix_socket(
"/tmp/ssh.sock",
client
.host()
.unix_socket(
self.client.host().unix_socket(
std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"),
)
.id()
.await?,
),
)
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock")
.with_new_file_opts(
"/root/.ssh/config",
dagger_sdk::ContainerWithNewFileOpts {
contents: Some(
"
Host *
User git
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
",
),
dagger_sdk::ContainerWithNewFileOpts {
owner: Some("root"),
permissions: Some(700),
expand: None,
},
)
.with_exec(vec![
);
let remote_url = res
.with_exec(vec!["git", "config", "--get", "remote.origin.url"])
.stdout()
.await?;
let res = if remote_url.starts_with("http") {
let new_remote_url = format!(
"ssh://git@{}",
remote_url
.trim()
.trim_start_matches("https://")
.trim_start_matches("http://")
);
println!("new remote_url: {}", new_remote_url);
res.with_exec(vec!["git", "remote", "set-url", "origin", &new_remote_url])
} else {
res
};
let res = res.with_exec(vec![
"cuddle-please",
"release",
&format!(
@ -337,10 +337,7 @@ Host *
},
]);
let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
res.sync().await?;
let please_out = res.stdout().await?;
println!("{please_out}");

View File

@ -0,0 +1,16 @@
[package]
name = "dagger-leptos"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,19 @@
[package]
name = "dagger-rust"
description = "A common set of components for dagger-sdk, which enables patterns such as build, test and publish"
version.workspace = true
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,366 @@
use std::path::PathBuf;
use crate::source::RustSource;
#[allow(dead_code)]
pub struct RustBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
target: impl AsRef<BuildTarget>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let rust_version = rust_version.as_ref();
let target = target.as_ref();
let profile = profile.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", &target.to_string()])
.with_exec(vec!["apt", "update"])
.with_exec(vec!["wget", "https://github.com/rui314/mold/releases/latest/download/mold-2.3.3-x86_64-linux.tar.gz"])
.with_exec("tar -xvf mold-2.3.3-x86_64-linux.tar.gz".split_whitespace().collect())
.with_exec("mv mold-2.3.3-x86_64-linux/bin/mold /usr/bin/mold".split_whitespace().collect())
.with_exec(deps);
let target_cache = self.client.cache_volume(format!(
"rust_target_{}_{}",
profile.to_string(),
target.to_string()
));
let target_str = target.to_string();
let mut build_options = vec!["cargo", "build", "--target", &target_str, "--workspace"];
if matches!(profile, BuildProfile::Release) {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_env_variable("SQLX_OFFLINE", "true")
.with_exec(vec!["cargo", "clean"])
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_debian_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_file(format!("/usr/local/bin/{}", bin_name), bin)
.with_exec(vec![bin_name, "--help"]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}
pub enum RustVersion {
Nightly,
Stable(String),
}
impl AsRef<RustVersion> for RustVersion {
fn as_ref(&self) -> &RustVersion {
self
}
}
impl ToString for RustVersion {
fn to_string(&self) -> String {
match self {
RustVersion::Nightly => "rustlang/rust:nightly".to_string(),
RustVersion::Stable(version) => format!("rust:{}", version),
}
}
}
pub enum BuildTarget {
LinuxAmd64,
LinuxArm64,
LinuxAmd64Musl,
LinuxArm64Musl,
MacOSAmd64,
MacOSArm64,
}
impl BuildTarget {
pub fn from_target(image: &SlimImage) -> Self {
match image {
SlimImage::Debian { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64,
BuildArchitecture::Arm64 => Self::LinuxArm64,
},
SlimImage::Alpine { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64Musl,
BuildArchitecture::Arm64 => Self::LinuxArm64Musl,
},
}
}
pub fn into_platform(&self) -> dagger_sdk::Platform {
let platform = match self {
BuildTarget::LinuxAmd64 => "linux/amd64",
BuildTarget::LinuxArm64 => "linux/arm64",
BuildTarget::LinuxAmd64Musl => "linux/amd64",
BuildTarget::LinuxArm64Musl => "linux/arm64",
BuildTarget::MacOSAmd64 => "darwin/amd64",
BuildTarget::MacOSArm64 => "darwin/arm64",
};
dagger_sdk::Platform(platform.into())
}
}
impl AsRef<BuildTarget> for BuildTarget {
fn as_ref(&self) -> &BuildTarget {
self
}
}
impl ToString for BuildTarget {
fn to_string(&self) -> String {
let target = match self {
BuildTarget::LinuxAmd64 => "x86_64-unknown-linux-gnu",
BuildTarget::LinuxArm64 => "aarch64-unknown-linux-gnu",
BuildTarget::LinuxAmd64Musl => "x86_64-unknown-linux-musl",
BuildTarget::LinuxArm64Musl => "aarch64-unknown-linux-musl",
BuildTarget::MacOSAmd64 => "x86_64-apple-darwin",
BuildTarget::MacOSArm64 => "aarch64-apple-darwin",
};
target.into()
}
}
pub enum BuildProfile {
Debug,
Release,
}
impl AsRef<BuildProfile> for BuildProfile {
fn as_ref(&self) -> &BuildProfile {
self
}
}
impl ToString for BuildProfile {
fn to_string(&self) -> String {
let profile = match self {
BuildProfile::Debug => "debug",
BuildProfile::Release => "release",
};
profile.into()
}
}
pub enum SlimImage {
Debian {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
Alpine {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
}
pub enum BuildArchitecture {
Amd64,
Arm64,
}

View File

@ -0,0 +1,264 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct HtmxBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl HtmxBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["wget", "https://github.com/rui314/mold/releases/latest/download/mold-2.3.3-x86_64-linux.tar.gz"])
.with_exec("tar -xvf mold-2.3.3-x86_64-linux.tar.gz".split_whitespace().collect())
.with_exec("mv mold /usr/bin/mold".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "sqlx-cli", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_htmx_{}", profile.to_string()));
let build_options = vec!["cargo", "sqlx", "prepare"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let postgres_password = "somepassword123";
let postgres = self
.client
.container()
.from("postgres:16.1")
.with_env_variable("POSTGRES_PASSWORD", postgres_password);
let postgres_service = postgres.with_exposed_port(5432);
let mut containers = Vec::new();
for container_image in images {
let container =
match &container_image {
SlimImage::Debian { image, deps, .. } => {
let _target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build = build_container
.with_service_binding("postgres", postgres_service.as_service())
.with_env_variable(
"DATABASE_URL",
"root:somepassword123@postgres:5432/postgres",
)
.with_exec(vec!["cargo", "sqlx", "migrate", "run"])
.with_exec(vec!["cargo", "sqlx", "prepare"])
.with_exec(vec!["cargo", "build", "--release", "--bin", bin_name]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!("target/{}/release/{}", target.to_string(), bin_name));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory("/mnt/src/target/site".to_string()),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@ -0,0 +1,251 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct LeptosBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl LeptosBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "cargo-leptos", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_leptos_{}", profile.to_string()));
let build_options = vec!["cargo", "leptos", "build", "--release", "-vv"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let _target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build = build_container.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"-vv",
]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory("/mnt/src/target/site".to_string()),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@ -0,0 +1,6 @@
pub mod build;
pub mod htmx;
pub mod leptos;
pub mod publish;
pub mod source;
pub mod test;

View File

@ -0,0 +1,40 @@
use std::sync::Arc;
pub struct RustPublish {
client: Arc<dagger_sdk::Query>,
}
impl RustPublish {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
Self { client }
}
pub async fn publish(
&self,
image: impl Into<String>,
tag: impl Into<String>,
containers: impl IntoIterator<Item = dagger_sdk::Container>,
) -> eyre::Result<()> {
let mut ids = Vec::new();
for container in containers.into_iter() {
let id = container.id().await?;
ids.push(id);
}
let image = self
.client
.container()
.publish_opts(
format!("{}:{}", image.into(), tag.into()),
dagger_sdk::ContainerPublishOpts {
platform_variants: Some(ids),
forced_compression: None,
media_types: None,
},
)
.await?;
println!("published: {}", image);
Ok(())
}
}

View File

@ -0,0 +1,201 @@
use std::path::{Path, PathBuf};
use eyre::Context;
pub struct RustSource {
client: dagger_sdk::Query,
exclude: Vec<String>,
}
impl RustSource {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"]
.into_iter()
.map(|s| s.to_string())
.collect(),
}
}
pub fn with_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude = exclude.into_iter().map(|s| s.into()).collect();
self
}
pub fn append_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude
.append(&mut exclude.into_iter().map(|s| s.into()).collect::<Vec<_>>());
self
}
pub async fn get_rust_src<T, I>(
&self,
source: Option<T>,
crate_paths: I,
) -> eyre::Result<(dagger_sdk::Directory, dagger_sdk::Directory)>
where
T: Into<PathBuf>,
T: Clone,
I: IntoIterator,
I::Item: Into<String>,
{
let source_path = match source.clone() {
Some(s) => s.into(),
None => PathBuf::from("."),
};
let (skeleton_files, _crates) = self
.get_rust_skeleton_files(&source_path, crate_paths)
.await?;
let src = self.get_src(source.clone()).await?;
let rust_src = self.get_rust_dep_src(source).await?;
let rust_src = rust_src.with_directory(".", skeleton_files);
Ok((src, rust_src))
}
pub async fn get_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(self.exclude.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let mut excludes = self.exclude.clone();
excludes.push("**/src".to_string());
excludes.push("**/tests".to_string());
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
//.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.exclude(excludes.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_target_src(
&self,
source_path: &Path,
container: dagger_sdk::Container,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<dagger_sdk::Directory> {
let (_skeleton_files, crates) = self
.get_rust_skeleton_files(source_path, crate_paths)
.await?;
let exclude = crates
.iter()
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let mut original_crates = crates.clone();
original_crates.extend(exclude);
let exclude = original_crates.iter().map(|c| c.as_str()).collect();
let incremental_dir = self.client.directory().with_directory_opts(
".",
container.directory("target"),
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
Ok(incremental_dir)
}
pub async fn get_rust_skeleton_files(
&self,
source_path: &Path,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let paths = crate_paths
.into_iter()
.map(|s| s.into())
.collect::<Vec<String>>();
let mut crates = Vec::new();
for path in paths {
if path.ends_with("/*") {
let mut dirs = tokio::fs::read_dir(source_path.join(path.trim_end_matches("/*")))
.await
.context(format!("failed to find path: {}", path.clone()))?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
crates.push(entry.path());
}
}
} else {
crates.push(PathBuf::from(path));
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = self.client.directory();
let mut crate_names = Vec::new();
for rust_crate in crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(
directory,
rust_crate.strip_prefix(source_path).unwrap_or(rust_crate),
)?;
}
Ok((directory, crate_names))
}
}

View File

@ -0,0 +1,75 @@
use std::{path::PathBuf};
use crate::{build::RustVersion, source::RustSource};
pub struct RustTest {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustTest {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn test(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<()> {
let rust_version = rust_version.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["apt", "update"])
.with_exec(deps);
let target_cache = self.client.cache_volume("rust_target_test".to_string());
let build_options = vec!["cargo", "build", "--workspace"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let test = rust_with_src.with_exec(vec!["cargo", "test"]);
let stdout = test.stdout().await?;
let stderr = test.stderr().await?;
println!("stdout: {}, stderr: {}", stdout, stderr);
test.sync().await?;
Ok(())
}
}

View File

@ -6,6 +6,8 @@ vars:
service: "dagger-components"
registry: kasperhermansen
cuddle_please_image: "kasperhermansen/cuddle-please:main-1691504183"
please:
project:
owner: kjuulh
@ -13,6 +15,8 @@ please:
branch: main
settings:
api_url: https://git.front.kjuulh.io
actions:
rust:
scripts:
"ci:main":

View File

@ -2,8 +2,7 @@ use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePleaseAction
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
dagger_sdk::connect(|client| async move {
DaggerCuddlePleaseAction::dagger(client.clone())
.execute_src(&CuddlePleaseSrcArgs {
cuddle_image: "kasperhermansen/cuddle-please:main-1691504183".into(),
@ -15,5 +14,9 @@ pub async fn main() -> eyre::Result<()> {
})
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -2,9 +2,8 @@ use dagger_cuddle_please::{models::CuddlePleaseArgs, DaggerCuddlePleaseAction};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
DaggerCuddlePleaseAction::dagger(client.clone())
dagger_sdk::connect(|client| async move {
DaggerCuddlePleaseAction::dagger(client)
.execute(&CuddlePleaseArgs {
repository: "dagger-components".into(),
owner: "kjuulh".into(),
@ -23,4 +22,7 @@ pub async fn main() -> eyre::Result<()> {
.await?;
Ok(())
})
.await?;
Ok(())
}

17
examples/htmx/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "htmx"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

18
examples/htmx/src/main.rs Normal file
View File

@ -0,0 +1,18 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -0,0 +1,13 @@
[package]
name = "leptos-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,97 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

Binary file not shown.

View File

@ -0,0 +1,64 @@
use dagger_rust::build::{RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let rust_build = dagger_rust::leptos::LeptosBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&[
"openssl",
"libssl-dev",
"pkg-config",
"musl-tools",
"ca-certificates",
],
vec![SlimImage::Debian {
image: "debian:bullseye".into(),
deps: vec![
"openssl".into(),
"libssl-dev".into(),
"pkg-config".into(),
"musl-tools".into(),
"ca-certificates".into(),
],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"hackernews_axum",
)
.await?;
let container = containers.first().unwrap();
container.directory("/mnt/app").export("output").await?;
let tunnel = client.host().tunnel(
container
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_exec(vec!["/mnt/app/hackernews_axum"])
.as_service(),
);
tunnel.start().await?;
let endpoint = tunnel
.endpoint_opts(
dagger_sdk::ServiceEndpointOptsBuilder::default()
.scheme("http")
.build()?,
)
.await?;
println!("running on: {endpoint}, press enter to stop");
std::io::stdin().read_line(&mut String::new()).unwrap();
Ok(())
})
.await?;
Ok(())
}

2809
examples/leptos-build/testdata/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[profile.release]
codegen-units = 1
lto = true
[[workspace.metadata.leptos]]
name = "hackernews_axum"
bin-package = "hackernews_axum"
lib-package = "hackernews_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./crates/hackernews_axum/style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "./crates/hackernews_axum/public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,97 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "hackernews_axum"

View File

@ -0,0 +1,7 @@
# Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,90 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
format!("https://node-hnapi.herokuapp.com/{path}")
}
pub fn user(path: &str) -> String {
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if e.g., we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = reqwest::get(path)
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Story {
pub id: usize,
pub title: String,
pub points: Option<i32>,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
#[serde(alias = "type")]
pub story_type: String,
pub url: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@ -0,0 +1,28 @@
use leptos::{view, Errors, For, IntoView, RwSignal, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children=move | (_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@ -0,0 +1,44 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@ -0,0 +1,63 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@ -0,0 +1,49 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
</>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
}

View File

@ -0,0 +1,54 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use hackernews_axum::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
}
}
}

View File

@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@ -0,0 +1,30 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@ -0,0 +1,156 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}
}}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
view! {
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@ -0,0 +1,125 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
</div>
</div>
}})}
</Suspense>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
}
})}
</div>
}
})}
</li>
}
}
fn pluralize(n: usize) -> &'static str {
if n == 1 {
" reply"
} else {
" replies"
}
}

View File

@ -0,0 +1,46 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || user.get().map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_any(),
Some(user) => view! {
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
})}
</Suspense>
</div>
}
}

View File

@ -0,0 +1,326 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@ -0,0 +1,13 @@
[package]
name = "rust-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,31 @@
use dagger_rust::build::{RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let rust_build = dagger_rust::build::RustBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
vec![SlimImage::Debian {
image: "debian:bookworm".into(),
deps: vec!["openssl".into()],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"example_bin",
)
.await?;
for container in containers {
container.sync().await?;
}
Ok(())
})
.await?;
Ok(())
}

View File

@ -0,0 +1,3 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,8 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

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

View File

@ -0,0 +1,13 @@
[package]
name = "rust-src"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,18 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -0,0 +1,13 @@
[package]
name = "rust-test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,20 @@
use dagger_rust::{build::RustVersion, test::RustTest};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
RustTest::new(client.clone())
.test(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
)
.await?;
Ok(())
})
.await?;
Ok(())
}

7
examples/rust-test/testdata/Cargo.lock generated vendored Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "example_bin"
version = "0.1.0"

View File

@ -0,0 +1,3 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,8 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,11 @@
fn main() {
println!("Hello, world!");
}
#[cfg(test)]
mod tests {
#[test]
fn test_main() {
assert_eq!(1, 1)
}
}

3
renovate.json Normal file
View File

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

View File

@ -9,4 +9,4 @@ if [[ -n "$CI_PREFIX" ]]; then
fi
$CMD_PREFIX main
$CMD_PREFIX main --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"

View File

@ -8,4 +8,4 @@ if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
fi
$CMD_PREFIX pull-request
$CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"

11
scripts/ci:release.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
CMD_PREFIX="cargo run -p ci --"
if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
fi
$CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"