Compare commits
No commits in common. "506912ff09e48dbcae9222041348d54b75894512" and "88c5c9c79051f616789b960f8d6b2d79e7d4a886" have entirely different histories.
506912ff09
...
88c5c9c790
28 changed files with 155 additions and 1747 deletions
448
Cargo.lock
generated
448
Cargo.lock
generated
|
|
@ -2,41 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aead"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes-gcm"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
|
||||||
dependencies = [
|
|
||||||
"aead",
|
|
||||||
"aes",
|
|
||||||
"cipher",
|
|
||||||
"ctr",
|
|
||||||
"ghash",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
|
|
@ -294,12 +259,6 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg_aliases"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
|
|
@ -382,19 +341,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"rand_core 0.6.4",
|
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctr"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
|
||||||
dependencies = [
|
|
||||||
"cipher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
|
@ -631,10 +580,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -644,11 +591,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -664,16 +609,6 @@ dependencies = [
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ghash"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
|
||||||
dependencies = [
|
|
||||||
"opaque-debug",
|
|
||||||
"polyval",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
|
@ -733,7 +668,6 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
name = "hiy-server"
|
name = "hiy-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -744,13 +678,12 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"reqwest",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http 0.5.2",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
@ -847,24 +780,6 @@ dependencies = [
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"want",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper-rustls"
|
|
||||||
version = "0.27.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"hyper",
|
|
||||||
"hyper-util",
|
|
||||||
"rustls 0.23.37",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tower-service",
|
|
||||||
"webpki-roots 1.0.6",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -873,21 +788,13 @@ version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"ipnet",
|
|
||||||
"libc",
|
|
||||||
"percent-encoding",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1043,22 +950,6 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ipnet"
|
|
||||||
version = "2.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iri-string"
|
|
||||||
version = "0.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
|
|
@ -1152,12 +1043,6 @@ version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1242,7 +1127,7 @@ dependencies = [
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -1283,12 +1168,6 @@ version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "opaque-debug"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
|
|
@ -1378,18 +1257,6 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "polyval"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"opaque-debug",
|
|
||||||
"universal-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
@ -1427,61 +1294,6 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.11.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.23.37",
|
|
||||||
"socket2",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.11.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"getrandom 0.3.4",
|
|
||||||
"lru-slab",
|
|
||||||
"rand 0.9.2",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.23.37",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.5.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
|
||||||
dependencies = [
|
|
||||||
"cfg_aliases",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"socket2",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
|
|
@ -1510,18 +1322,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
|
||||||
dependencies = [
|
|
||||||
"rand_chacha 0.9.0",
|
|
||||||
"rand_core 0.9.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1531,17 +1333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core 0.9.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1553,15 +1345,6 @@ dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.3.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -1597,44 +1380,6 @@ version = "0.8.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "reqwest"
|
|
||||||
version = "0.12.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"bytes",
|
|
||||||
"futures-core",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper",
|
|
||||||
"hyper-rustls",
|
|
||||||
"hyper-util",
|
|
||||||
"js-sys",
|
|
||||||
"log",
|
|
||||||
"percent-encoding",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn",
|
|
||||||
"rustls 0.23.37",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"sync_wrapper",
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tower",
|
|
||||||
"tower-http 0.6.8",
|
|
||||||
"tower-service",
|
|
||||||
"url",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
"webpki-roots 1.0.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
|
|
@ -1662,19 +1407,13 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
"signature",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "2.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -1695,24 +1434,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-webpki 0.101.7",
|
"rustls-webpki",
|
||||||
"sct",
|
"sct",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls"
|
|
||||||
version = "0.23.37"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"rustls-webpki 0.103.10",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1722,16 +1447,6 @@ dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-pki-types"
|
|
||||||
version = "1.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
|
||||||
dependencies = [
|
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.101.7"
|
version = "0.101.7"
|
||||||
|
|
@ -1742,17 +1457,6 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-webpki"
|
|
||||||
version = "0.103.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
|
||||||
dependencies = [
|
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"untrusted",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|
@ -1907,7 +1611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2002,19 +1706,19 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rustls 0.21.12",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlformat",
|
"sqlformat",
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"webpki-roots 0.25.4",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2086,7 +1790,7 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
|
|
@ -2094,7 +1798,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
@ -2126,14 +1830,14 @@ dependencies = [
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
@ -2212,9 +1916,6 @@ name = "sync_wrapper"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
|
|
@ -2246,16 +1947,7 @@ version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 1.0.69",
|
"thiserror-impl",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl 2.0.18",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2269,17 +1961,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
|
|
@ -2342,16 +2023,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-rustls"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
|
||||||
dependencies = [
|
|
||||||
"rustls 0.23.37",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
|
|
@ -2396,24 +2067,6 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-http"
|
|
||||||
version = "0.6.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"bytes",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"iri-string",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tower",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -2488,12 +2141,6 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "try-lock"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
|
@ -2545,16 +2192,6 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "universal-hash"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -2570,7 +2207,6 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2614,15 +2250,6 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "want"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
|
||||||
dependencies = [
|
|
||||||
"try-lock",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|
@ -2666,20 +2293,6 @@ dependencies = [
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-futures"
|
|
||||||
version = "0.4.64"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"futures-util",
|
|
||||||
"js-sys",
|
|
||||||
"once_cell",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.114"
|
version = "0.2.114"
|
||||||
|
|
@ -2746,41 +2359,12 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.91"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-time"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.25.4"
|
version = "0.25.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki-roots"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,4 @@
|
||||||
* Runs on your hardware (linux vm/host)
|
* Runs on your hardware (linux vm/host)
|
||||||
* Integrate with git using github webhooks or add your own git remote
|
* Integrate with git using github webhooks or add your own git remote
|
||||||
* automatic redeployment after git push
|
* automatic redeployment after git push
|
||||||
* Builtin ssl. Automatically provisioned using let's encrypt.
|
* Builtin ssl. Automatically provisioned using let's encrypt.
|
||||||
* Caddy reverse proxy
|
|
||||||
|
|
@ -2,16 +2,8 @@
|
||||||
# HIY Build Engine
|
# HIY Build Engine
|
||||||
# Environment variables injected by hiy-server:
|
# Environment variables injected by hiy-server:
|
||||||
# APP_ID, APP_NAME, REPO_URL, BRANCH, PORT, ENV_FILE, SHA, BUILD_DIR
|
# APP_ID, APP_NAME, REPO_URL, BRANCH, PORT, ENV_FILE, SHA, BUILD_DIR
|
||||||
# MEMORY_LIMIT (e.g. "512m"), CPU_LIMIT (e.g. "0.5")
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Never prompt for git credentials — fail immediately if auth is missing.
|
|
||||||
export GIT_TERMINAL_PROMPT=0
|
|
||||||
|
|
||||||
# Defaults — overridden by per-app settings stored in the control plane.
|
|
||||||
MEMORY_LIMIT="${MEMORY_LIMIT:-512m}"
|
|
||||||
CPU_LIMIT="${CPU_LIMIT:-0.5}"
|
|
||||||
|
|
||||||
log() { echo "[hiy] $*"; }
|
log() { echo "[hiy] $*"; }
|
||||||
|
|
||||||
log "=== HostItYourself Build Engine ==="
|
log "=== HostItYourself Build Engine ==="
|
||||||
|
|
@ -21,33 +13,17 @@ log "Branch: $BRANCH"
|
||||||
log "Build dir: $BUILD_DIR"
|
log "Build dir: $BUILD_DIR"
|
||||||
|
|
||||||
# ── 1. Clone or pull ───────────────────────────────────────────────────────────
|
# ── 1. Clone or pull ───────────────────────────────────────────────────────────
|
||||||
# Build an authenticated URL when a git token is set (private repos).
|
|
||||||
# GIT_TOKEN is passed by hiy-server and never echoed here.
|
|
||||||
CLONE_URL="$REPO_URL"
|
|
||||||
if [ -n "${GIT_TOKEN:-}" ]; then
|
|
||||||
case "$REPO_URL" in
|
|
||||||
https://*)
|
|
||||||
CLONE_URL="https://x-access-token:${GIT_TOKEN}@${REPO_URL#https://}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
if [ -d ".git" ]; then
|
if [ -d ".git" ]; then
|
||||||
log "Updating existing clone…"
|
log "Updating existing clone…"
|
||||||
git remote set-url origin "$CLONE_URL"
|
|
||||||
git fetch origin "$BRANCH" --depth=50
|
git fetch origin "$BRANCH" --depth=50
|
||||||
git checkout "$BRANCH"
|
git checkout "$BRANCH"
|
||||||
git reset --hard "origin/$BRANCH"
|
git reset --hard "origin/$BRANCH"
|
||||||
# Strip credentials from the stored remote so they don't sit in .git/config.
|
|
||||||
git remote set-url origin "$REPO_URL"
|
|
||||||
else
|
else
|
||||||
log "Cloning repository…"
|
log "Cloning repository…"
|
||||||
git clone --depth=50 --branch "$BRANCH" "$CLONE_URL" .
|
git clone --depth=50 --branch "$BRANCH" "$REPO_URL" .
|
||||||
# Strip credentials from the stored remote so they don't sit in .git/config.
|
|
||||||
git remote set-url origin "$REPO_URL"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ACTUAL_SHA=$(git rev-parse HEAD)
|
ACTUAL_SHA=$(git rev-parse HEAD)
|
||||||
|
|
@ -129,8 +105,7 @@ podman run --detach \
|
||||||
--label "hiy.app=${APP_ID}" \
|
--label "hiy.app=${APP_ID}" \
|
||||||
--label "hiy.port=${PORT}" \
|
--label "hiy.port=${PORT}" \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
--memory="${MEMORY_LIMIT}" \
|
--cpus="0.5" \
|
||||||
--cpus="${CPU_LIMIT}" \
|
|
||||||
"$IMAGE_TAG"
|
"$IMAGE_TAG"
|
||||||
|
|
||||||
# ── 6. Update Caddy via its admin API ─────────────────────────────────────────
|
# ── 6. Update Caddy via its admin API ─────────────────────────────────────────
|
||||||
|
|
@ -149,20 +124,11 @@ if curl --silent --fail "${CADDY_API}/config/" >/dev/null 2>&1; then
|
||||||
else
|
else
|
||||||
ROUTES_URL="${CADDY_API}/config/apps/http/servers/${CADDY_SERVER}/routes"
|
ROUTES_URL="${CADDY_API}/config/apps/http/servers/${CADDY_SERVER}/routes"
|
||||||
|
|
||||||
# Route JSON: public apps use plain reverse_proxy; private apps use forward_auth.
|
# Route JSON uses Caddy's forward_auth pattern:
|
||||||
if [ "${IS_PUBLIC:-0}" = "1" ]; then
|
# 1. HIY server checks the session cookie and app-level permission at /auth/verify
|
||||||
ROUTE_JSON=$(python3 -c "
|
# 2. On 2xx → Caddy proxies to the app container
|
||||||
import json, sys
|
# 3. On anything else (e.g. 302 redirect to /login) → Caddy passes through to the client
|
||||||
upstream = sys.argv[1]
|
ROUTE_JSON=$(python3 -c "
|
||||||
app_host = sys.argv[2]
|
|
||||||
route = {
|
|
||||||
'match': [{'host': [app_host]}],
|
|
||||||
'handle': [{'handler': 'reverse_proxy', 'upstreams': [{'dial': upstream}]}]
|
|
||||||
}
|
|
||||||
print(json.dumps(route))
|
|
||||||
" "${UPSTREAM}" "${APP_ID}.${DOMAIN_SUFFIX}")
|
|
||||||
else
|
|
||||||
ROUTE_JSON=$(python3 -c "
|
|
||||||
import json, sys
|
import json, sys
|
||||||
upstream = sys.argv[1]
|
upstream = sys.argv[1]
|
||||||
app_host = sys.argv[2]
|
app_host = sys.argv[2]
|
||||||
|
|
@ -196,7 +162,6 @@ route = {
|
||||||
}
|
}
|
||||||
print(json.dumps(route))
|
print(json.dumps(route))
|
||||||
" "${UPSTREAM}" "${APP_ID}.${DOMAIN_SUFFIX}")
|
" "${UPSTREAM}" "${APP_ID}.${DOMAIN_SUFFIX}")
|
||||||
fi
|
|
||||||
# Upsert the route for this app.
|
# Upsert the route for this app.
|
||||||
ROUTES=$(curl --silent --fail "${ROUTES_URL}" 2>/dev/null || echo "[]")
|
ROUTES=$(curl --silent --fail "${ROUTES_URL}" 2>/dev/null || echo "[]")
|
||||||
# Remove existing route for the same host, rebuild list, keep dashboard as catch-all.
|
# Remove existing route for the same host, rebuild list, keep dashboard as catch-all.
|
||||||
|
|
|
||||||
10
docs/plan.md
10
docs/plan.md
|
|
@ -261,11 +261,11 @@ hostityourself/
|
||||||
- [ ] Deploy history
|
- [ ] Deploy history
|
||||||
|
|
||||||
### M4 — Hardening (Week 5)
|
### M4 — Hardening (Week 5)
|
||||||
- [x] Env var encryption at rest (AES-256-GCM via `HIY_SECRET_KEY`; transparent plaintext passthrough for migration)
|
- [ ] Env var encryption at rest
|
||||||
- [x] Resource limits on containers (per-app `memory_limit` + `cpu_limit`; defaults 512m / 0.5 CPU)
|
- [ ] Resource limits on containers
|
||||||
- [x] Netdata + Gatus setup (`monitoring` compose profile; `infra/gatus.yml`)
|
- [ ] Netdata + Gatus setup
|
||||||
- [x] Backup cron job (`infra/backup.sh` — SQLite dump + env files + git repos; local + rclone remote)
|
- [ ] Backup cron job
|
||||||
- [x] Dashboard auth (multi-user sessions, bcrypt, API keys — done in earlier milestone)
|
- [ ] Dashboard auth
|
||||||
|
|
||||||
### M5 — Polish (Week 6)
|
### M5 — Polish (Week 6)
|
||||||
- [ ] Buildpack detection (Dockerfile / Node / Python / static)
|
- [ ] Buildpack detection (Dockerfile / Node / Python / static)
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,7 @@ ssh pi@hiypi.local
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt full-upgrade -y
|
sudo apt update && sudo apt full-upgrade -y
|
||||||
sudo apt install -y git curl ufw fail2ban unattended-upgrades podman python3 pipx aardvark-dns sqlite3
|
sudo apt install -y git curl ufw fail2ban unattended-upgrades
|
||||||
pipx install podman-compose
|
|
||||||
pipx ensurepath
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Static IP (optional but recommended)
|
### Static IP (optional but recommended)
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,3 @@ HIY_ADMIN_PASS=changeme
|
||||||
# Postgres admin password — used by the shared cluster.
|
# Postgres admin password — used by the shared cluster.
|
||||||
# App schemas get their own scoped users; this password never leaves the server.
|
# App schemas get their own scoped users; this password never leaves the server.
|
||||||
POSTGRES_PASSWORD=changeme
|
POSTGRES_PASSWORD=changeme
|
||||||
|
|
||||||
# Forgejo (optional — only needed if you add the forgejo service to docker-compose.yml).
|
|
||||||
FORGEJO_DB_PASSWORD=changeme
|
|
||||||
FORGEJO_DOMAIN=git.yourdomain.com
|
|
||||||
# Actions runner registration token — obtain from Forgejo:
|
|
||||||
# Site Administration → Actions → Runners → Create new runner
|
|
||||||
FORGEJO_RUNNER_TOKEN=
|
|
||||||
|
|
||||||
# ── Backup (infra/backup.sh) ──────────────────────────────────────────────────
|
|
||||||
# Local directory to store backup archives.
|
|
||||||
HIY_BACKUP_DIR=/mnt/usb/hiy-backups
|
|
||||||
# Optional rclone remote (e.g. "b2:mybucket/hiy", "s3:mybucket/hiy").
|
|
||||||
# Requires rclone installed and configured. Leave blank to skip remote upload.
|
|
||||||
HIY_BACKUP_REMOTE=
|
|
||||||
# How many days to keep local archives (default 30).
|
|
||||||
HIY_BACKUP_RETAIN_DAYS=30
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,66 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# ── Build stage ───────────────────────────────────────────────────────────────
|
# ── Build stage ───────────────────────────────────────────────────────────────
|
||||||
# Native build: Cargo targets the host platform automatically.
|
# Run the compiler on the *build* host; cross-compile to target when needed.
|
||||||
# No --target flag means no cross-compiler confusion regardless of which
|
FROM --platform=$BUILDPLATFORM rust:1.94-slim-bookworm AS builder
|
||||||
# arch podman-compose runs on (x86_64, arm64, armv7…).
|
|
||||||
FROM docker.io/library/rust:1.94-slim-bookworm AS builder
|
|
||||||
|
|
||||||
# gcc is required by cc-rs (used by aes-gcm / ring build scripts).
|
ARG BUILDPLATFORM
|
||||||
# rust:slim does not include a C compiler.
|
ARG TARGETPLATFORM
|
||||||
RUN apt-get update && apt-get install -y gcc pkg-config && \
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
|
# Install cross-compilation toolchains only when actually cross-compiling.
|
||||||
|
RUN apt-get update && apt-get install -y pkg-config && \
|
||||||
|
if [ "${BUILDPLATFORM}" != "${TARGETPLATFORM}" ]; then \
|
||||||
|
case "${TARGETARCH}:${TARGETVARIANT}" in \
|
||||||
|
"arm64:") apt-get install -y gcc-aarch64-linux-gnu ;; \
|
||||||
|
"arm:v7") apt-get install -y gcc-arm-linux-gnueabihf ;; \
|
||||||
|
"arm:v6") apt-get install -y gcc-arm-linux-gnueabi ;; \
|
||||||
|
esac; \
|
||||||
|
fi && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Map TARGETARCH + TARGETVARIANT → Rust target triple, then install it.
|
||||||
|
RUN case "${TARGETARCH}:${TARGETVARIANT}" in \
|
||||||
|
"amd64:") echo x86_64-unknown-linux-gnu ;; \
|
||||||
|
"arm64:") echo aarch64-unknown-linux-gnu ;; \
|
||||||
|
"arm:v7") echo armv7-unknown-linux-gnueabihf ;; \
|
||||||
|
"arm:v6") echo arm-unknown-linux-gnueabi ;; \
|
||||||
|
*) echo x86_64-unknown-linux-gnu ;; \
|
||||||
|
esac > /rust_target && \
|
||||||
|
rustup target add "$(cat /rust_target)"
|
||||||
|
|
||||||
|
# Tell Cargo which cross-linker to use (ignored on native builds).
|
||||||
|
RUN mkdir -p /root/.cargo && printf '\
|
||||||
|
[target.aarch64-unknown-linux-gnu]\n\
|
||||||
|
linker = "aarch64-linux-gnu-gcc"\n\
|
||||||
|
\n\
|
||||||
|
[target.armv7-unknown-linux-gnueabihf]\n\
|
||||||
|
linker = "arm-linux-gnueabihf-gcc"\n\
|
||||||
|
\n\
|
||||||
|
[target.arm-unknown-linux-gnueabi]\n\
|
||||||
|
linker = "arm-linux-gnueabi-gcc"\n' >> /root/.cargo/config.toml
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Cache dependency compilation separately from application source.
|
# Cache dependencies separately from source.
|
||||||
COPY Cargo.toml Cargo.lock* ./
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
COPY server/Cargo.toml ./server/
|
COPY server/Cargo.toml ./server/
|
||||||
RUN mkdir -p server/src && echo 'fn main(){}' > server/src/main.rs
|
RUN mkdir -p server/src && echo 'fn main(){}' > server/src/main.rs
|
||||||
RUN cargo build --release -p hiy-server 2>/dev/null || true
|
RUN TARGET=$(cat /rust_target) && \
|
||||||
|
cargo build --release --target "$TARGET" -p hiy-server 2>/dev/null || true
|
||||||
RUN rm -f server/src/main.rs
|
RUN rm -f server/src/main.rs
|
||||||
|
|
||||||
# Build actual source.
|
# Build actual source.
|
||||||
COPY server/src ./server/src
|
COPY server/src ./server/src
|
||||||
COPY server/templates ./server/templates
|
RUN TARGET=$(cat /rust_target) && \
|
||||||
RUN touch server/src/main.rs && \
|
touch server/src/main.rs && \
|
||||||
cargo build --release -p hiy-server
|
cargo build --release --target "$TARGET" -p hiy-server
|
||||||
|
|
||||||
|
# Normalise binary location so the runtime stage doesn't need to know the target.
|
||||||
|
RUN cp /build/target/"$(cat /rust_target)"/release/hiy-server /usr/local/bin/hiy-server
|
||||||
|
|
||||||
# ── Runtime stage ─────────────────────────────────────────────────────────────
|
# ── Runtime stage ─────────────────────────────────────────────────────────────
|
||||||
FROM docker.io/library/debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
|
@ -37,7 +71,7 @@ RUN apt-get update && apt-get install -y \
|
||||||
podman \
|
podman \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/hiy-server /usr/local/bin/hiy-server
|
COPY --from=builder /usr/local/bin/hiy-server /usr/local/bin/hiy-server
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# auto-update.sh — pull latest changes and restart affected services.
|
|
||||||
# Run by the hiy-update.timer systemd user unit every 5 minutes.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
log() { echo "[hiy-update] $(date '+%H:%M:%S') $*"; }
|
|
||||||
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
# Fetch without touching the working tree.
|
|
||||||
git fetch origin 2>&1 | sed 's/^/[git] /' || { log "git fetch failed — skipping"; exit 0; }
|
|
||||||
|
|
||||||
LOCAL=$(git rev-parse HEAD)
|
|
||||||
REMOTE=$(git rev-parse "@{u}" 2>/dev/null || echo "$LOCAL")
|
|
||||||
|
|
||||||
if [ "$LOCAL" = "$REMOTE" ]; then
|
|
||||||
log "Already up to date ($LOCAL)."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "New commits detected — pulling ($LOCAL → $REMOTE)…"
|
|
||||||
git pull 2>&1 | sed 's/^/[git] /'
|
|
||||||
|
|
||||||
# Determine which services need restarting based on what changed.
|
|
||||||
CHANGED=$(git diff --name-only "$LOCAL" "$REMOTE")
|
|
||||||
log "Changed files: $(echo "$CHANGED" | tr '\n' ' ')"
|
|
||||||
|
|
||||||
# Always rebuild the server if any server-side code changed.
|
|
||||||
SERVER_CHANGED=$(echo "$CHANGED" | grep -E '^server/|^Cargo' || true)
|
|
||||||
COMPOSE_CHANGED=$(echo "$CHANGED" | grep '^infra/docker-compose' || true)
|
|
||||||
CADDY_CHANGED=$(echo "$CHANGED" | grep '^proxy/Caddyfile' || true)
|
|
||||||
|
|
||||||
COMPOSE_CMD="podman compose --env-file $REPO_ROOT/.env -f $SCRIPT_DIR/docker-compose.yml"
|
|
||||||
|
|
||||||
if [ -n "$COMPOSE_CHANGED" ]; then
|
|
||||||
log "docker-compose.yml changed — restarting full stack…"
|
|
||||||
$COMPOSE_CMD up -d
|
|
||||||
elif [ -n "$SERVER_CHANGED" ]; then
|
|
||||||
log "Server code changed — rebuilding server…"
|
|
||||||
$COMPOSE_CMD up -d --build server
|
|
||||||
elif [ -n "$CADDY_CHANGED" ]; then
|
|
||||||
log "Caddyfile changed — reloading Caddy…"
|
|
||||||
$COMPOSE_CMD exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
else
|
|
||||||
log "No service restart needed for these changes."
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Done."
|
|
||||||
162
infra/backup.sh
162
infra/backup.sh
|
|
@ -1,162 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# HIY daily backup script
|
|
||||||
#
|
|
||||||
# What is backed up:
|
|
||||||
# 1. SQLite database (hiy.db) — apps, deploys, env vars, users
|
|
||||||
# 2. Env files — per-deploy decrypted env files
|
|
||||||
# 3. Git repos — bare repos for git-push deploys
|
|
||||||
# 4. Postgres — pg_dumpall (hiy + forgejo databases)
|
|
||||||
# 5. Forgejo data volume — repositories, avatars, LFS objects
|
|
||||||
# 6. Caddy TLS certificates — caddy-data volume
|
|
||||||
# 7. .env file — secrets (handle the archive with care)
|
|
||||||
#
|
|
||||||
# Destination options (mutually exclusive; set one):
|
|
||||||
# HIY_BACKUP_DIR — local path (e.g. /mnt/usb/hiy-backups, default /tmp/hiy-backups)
|
|
||||||
# HIY_BACKUP_REMOTE — rclone remote:path (e.g. "b2:mybucket/hiy")
|
|
||||||
# requires rclone installed and configured
|
|
||||||
#
|
|
||||||
# Retention: 30 days local (remote retention managed by the storage provider).
|
|
||||||
#
|
|
||||||
# Suggested cron (run as the same user that owns the containers):
|
|
||||||
# 0 3 * * * /path/to/infra/backup.sh >> /var/log/hiy-backup.log 2>&1
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ── Load .env ──────────────────────────────────────────────────────────────────
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
ENV_FILE="${SCRIPT_DIR}/../.env"
|
|
||||||
if [ -f "$ENV_FILE" ]; then
|
|
||||||
set -a; source "$ENV_FILE"; set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Config ─────────────────────────────────────────────────────────────────────
|
|
||||||
HIY_DATA_DIR="${HIY_DATA_DIR:-/data}"
|
|
||||||
BACKUP_DIR="${HIY_BACKUP_DIR:-/tmp/hiy-backups}"
|
|
||||||
BACKUP_REMOTE="${HIY_BACKUP_REMOTE:-}"
|
|
||||||
RETAIN_DAYS="${HIY_BACKUP_RETAIN_DAYS:-30}"
|
|
||||||
|
|
||||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
||||||
ARCHIVE_NAME="hiy-backup-${TIMESTAMP}.tar.gz"
|
|
||||||
STAGING="${BACKUP_DIR}/staging-${TIMESTAMP}"
|
|
||||||
|
|
||||||
log() { echo "[hiy-backup] $(date '+%H:%M:%S') $*"; }
|
|
||||||
|
|
||||||
log "=== HIY Backup ==="
|
|
||||||
log "Data dir : ${HIY_DATA_DIR}"
|
|
||||||
log "Staging : ${STAGING}"
|
|
||||||
|
|
||||||
mkdir -p "${STAGING}"
|
|
||||||
|
|
||||||
# ── Helper: find a running container by compose service label ──────────────────
|
|
||||||
find_container() {
|
|
||||||
local service="$1"
|
|
||||||
podman ps --filter "label=com.docker.compose.service=${service}" \
|
|
||||||
--format '{{.Names}}' | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 1. SQLite ──────────────────────────────────────────────────────────────────
|
|
||||||
log "--- SQLite ---"
|
|
||||||
SERVER_CTR=$(find_container server)
|
|
||||||
if [ -n "${SERVER_CTR}" ]; then
|
|
||||||
log "Copying hiy.db from container ${SERVER_CTR}…"
|
|
||||||
podman cp "${SERVER_CTR}:${HIY_DATA_DIR}/hiy.db" "${STAGING}/hiy.db"
|
|
||||||
log "Dumping hiy.db…"
|
|
||||||
sqlite3 "${STAGING}/hiy.db" .dump > "${STAGING}/hiy.sql"
|
|
||||||
rm "${STAGING}/hiy.db"
|
|
||||||
elif [ -f "${HIY_DATA_DIR}/hiy.db" ]; then
|
|
||||||
log "Server container not running — dumping from host path…"
|
|
||||||
sqlite3 "${HIY_DATA_DIR}/hiy.db" .dump > "${STAGING}/hiy.sql"
|
|
||||||
else
|
|
||||||
log "WARNING: hiy.db not found — skipping SQLite dump"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 2. Env files ───────────────────────────────────────────────────────────────
|
|
||||||
log "--- Env files ---"
|
|
||||||
if [ -n "${SERVER_CTR}" ]; then
|
|
||||||
podman exec "${SERVER_CTR}" sh -c \
|
|
||||||
"[ -d ${HIY_DATA_DIR}/envs ] && tar -C ${HIY_DATA_DIR} -czf - envs" \
|
|
||||||
> "${STAGING}/envs.tar.gz" 2>/dev/null || true
|
|
||||||
elif [ -d "${HIY_DATA_DIR}/envs" ]; then
|
|
||||||
tar -czf "${STAGING}/envs.tar.gz" -C "${HIY_DATA_DIR}" envs
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. Git repos ───────────────────────────────────────────────────────────────
|
|
||||||
log "--- Git repos ---"
|
|
||||||
if [ -n "${SERVER_CTR}" ]; then
|
|
||||||
podman exec "${SERVER_CTR}" sh -c \
|
|
||||||
"[ -d ${HIY_DATA_DIR}/repos ] && tar -C ${HIY_DATA_DIR} -czf - repos" \
|
|
||||||
> "${STAGING}/repos.tar.gz" 2>/dev/null || true
|
|
||||||
elif [ -d "${HIY_DATA_DIR}/repos" ]; then
|
|
||||||
tar -czf "${STAGING}/repos.tar.gz" -C "${HIY_DATA_DIR}" repos
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. Postgres ────────────────────────────────────────────────────────────────
|
|
||||||
log "--- Postgres ---"
|
|
||||||
PG_CTR=$(find_container postgres)
|
|
||||||
if [ -n "${PG_CTR}" ]; then
|
|
||||||
log "Running pg_dumpall via container ${PG_CTR}…"
|
|
||||||
podman exec "${PG_CTR}" pg_dumpall -U hiy_admin \
|
|
||||||
> "${STAGING}/postgres.sql"
|
|
||||||
else
|
|
||||||
log "WARNING: postgres container not running — skipping Postgres dump"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 5. Forgejo data volume ─────────────────────────────────────────────────────
|
|
||||||
log "--- Forgejo volume ---"
|
|
||||||
if podman volume exists forgejo-data 2>/dev/null; then
|
|
||||||
log "Exporting forgejo-data volume…"
|
|
||||||
podman volume export forgejo-data > "${STAGING}/forgejo-data.tar"
|
|
||||||
else
|
|
||||||
log "forgejo-data volume not found — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. Caddy TLS certificates ──────────────────────────────────────────────────
|
|
||||||
log "--- Caddy volume ---"
|
|
||||||
if podman volume exists caddy-data 2>/dev/null; then
|
|
||||||
log "Exporting caddy-data volume…"
|
|
||||||
podman volume export caddy-data > "${STAGING}/caddy-data.tar"
|
|
||||||
else
|
|
||||||
log "caddy-data volume not found — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 7. .env file ───────────────────────────────────────────────────────────────
|
|
||||||
log "--- .env ---"
|
|
||||||
if [ -f "${ENV_FILE}" ]; then
|
|
||||||
cp "${ENV_FILE}" "${STAGING}/dot-env"
|
|
||||||
log "WARNING: archive contains plaintext secrets — store it securely"
|
|
||||||
else
|
|
||||||
log ".env not found at ${ENV_FILE} — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Create archive ─────────────────────────────────────────────────────────────
|
|
||||||
mkdir -p "${BACKUP_DIR}"
|
|
||||||
ARCHIVE_PATH="${BACKUP_DIR}/${ARCHIVE_NAME}"
|
|
||||||
log "Creating archive: ${ARCHIVE_PATH}"
|
|
||||||
tar -czf "${ARCHIVE_PATH}" -C "${STAGING}" .
|
|
||||||
rm -rf "${STAGING}"
|
|
||||||
|
|
||||||
ARCHIVE_SIZE=$(du -sh "${ARCHIVE_PATH}" | cut -f1)
|
|
||||||
log "Archive size: ${ARCHIVE_SIZE}"
|
|
||||||
|
|
||||||
# ── Upload to remote (optional) ────────────────────────────────────────────────
|
|
||||||
if [ -n "${BACKUP_REMOTE}" ]; then
|
|
||||||
if command -v rclone &>/dev/null; then
|
|
||||||
log "Uploading to ${BACKUP_REMOTE}…"
|
|
||||||
#use patched rclone for now
|
|
||||||
/home/sander/dev/rclone/rclone copy --transfers 1 --retries 5 "${ARCHIVE_PATH}" "${BACKUP_REMOTE}/"
|
|
||||||
log "Upload complete."
|
|
||||||
else
|
|
||||||
log "WARNING: HIY_BACKUP_REMOTE is set but rclone is not installed — skipping"
|
|
||||||
log "Install: https://rclone.org/install/"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Rotate old local backups ───────────────────────────────────────────────────
|
|
||||||
log "Removing local backups older than ${RETAIN_DAYS} days…"
|
|
||||||
find "${BACKUP_DIR}" -maxdepth 1 -name 'hiy-backup-*.tar.gz' \
|
|
||||||
-mtime "+${RETAIN_DAYS}" -delete
|
|
||||||
|
|
||||||
REMAINING=$(find "${BACKUP_DIR}" -maxdepth 1 -name 'hiy-backup-*.tar.gz' | wc -l)
|
|
||||||
log "Local backups retained: ${REMAINING}"
|
|
||||||
|
|
||||||
log "=== Backup complete: ${ARCHIVE_NAME} ==="
|
|
||||||
|
|
@ -12,7 +12,7 @@ services:
|
||||||
# rootful: /run/podman/podman.sock
|
# rootful: /run/podman/podman.sock
|
||||||
# rootless: /run/user/<UID>/podman/podman.sock (start.sh sets this)
|
# rootless: /run/user/<UID>/podman/podman.sock (start.sh sets this)
|
||||||
podman-proxy:
|
podman-proxy:
|
||||||
image: docker.io/alpine/socat
|
image: alpine/socat
|
||||||
command: tcp-listen:2375,fork,reuseaddr unix-connect:/podman.sock
|
command: tcp-listen:2375,fork,reuseaddr unix-connect:/podman.sock
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -62,71 +62,20 @@ services:
|
||||||
|
|
||||||
# ── Shared Postgres ───────────────────────────────────────────────────────
|
# ── Shared Postgres ───────────────────────────────────────────────────────
|
||||||
postgres:
|
postgres:
|
||||||
image: docker.io/library/postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: hiy
|
POSTGRES_DB: hiy
|
||||||
POSTGRES_USER: hiy_admin
|
POSTGRES_USER: hiy_admin
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
FORGEJO_DB_PASSWORD: ${FORGEJO_DB_PASSWORD}
|
|
||||||
volumes:
|
volumes:
|
||||||
- hiy-pg-data:/var/lib/postgresql/data
|
- hiy-pg-data:/var/lib/postgresql/data
|
||||||
# SQL files here run once on first init (ignored if data volume already exists).
|
|
||||||
- ./postgres-init:/docker-entrypoint-initdb.d:ro
|
|
||||||
networks:
|
|
||||||
- hiy-net
|
|
||||||
|
|
||||||
# ── Forgejo (self-hosted Git) ──────────────────────────────────────────────
|
|
||||||
forgejo:
|
|
||||||
image: codeberg.org/forgejo/forgejo:10
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
USER_UID: 1000
|
|
||||||
USER_GID: 1000
|
|
||||||
FORGEJO__database__DB_TYPE: postgres
|
|
||||||
FORGEJO__database__HOST: postgres:5432
|
|
||||||
FORGEJO__database__NAME: forgejo
|
|
||||||
FORGEJO__database__USER: forgejo
|
|
||||||
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
|
|
||||||
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
|
|
||||||
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/
|
|
||||||
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
|
|
||||||
# Skip the first-run wizard — everything is configured via env vars above.
|
|
||||||
FORGEJO__security__INSTALL_LOCK: "true"
|
|
||||||
# Enable Actions.
|
|
||||||
FORGEJO__actions__ENABLED: "true"
|
|
||||||
volumes:
|
|
||||||
- forgejo-data:/data
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- hiy-net
|
|
||||||
|
|
||||||
# ── Forgejo Actions runner ─────────────────────────────────────────────────
|
|
||||||
# Obtain FORGEJO_RUNNER_TOKEN from Forgejo:
|
|
||||||
# Site Administration → Actions → Runners → Create new runner
|
|
||||||
act_runner:
|
|
||||||
image: data.forgejo.org/forgejo/runner:6
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["/entrypoint.sh"]
|
|
||||||
environment:
|
|
||||||
FORGEJO_INSTANCE_URL: http://forgejo:3000
|
|
||||||
FORGEJO_RUNNER_TOKEN: ${FORGEJO_RUNNER_TOKEN}
|
|
||||||
FORGEJO_RUNNER_NAME: hiy-runner
|
|
||||||
# Give the runner access to Podman so CI jobs can build/run containers.
|
|
||||||
DOCKER_HOST: tcp://podman-proxy:2375
|
|
||||||
volumes:
|
|
||||||
- act_runner_data:/data
|
|
||||||
- ./runner-entrypoint.sh:/entrypoint.sh:ro
|
|
||||||
depends_on:
|
|
||||||
- forgejo
|
|
||||||
- podman-proxy
|
|
||||||
networks:
|
networks:
|
||||||
- hiy-net
|
- hiy-net
|
||||||
|
|
||||||
# ── Reverse proxy ─────────────────────────────────────────────────────────
|
# ── Reverse proxy ─────────────────────────────────────────────────────────
|
||||||
caddy:
|
caddy:
|
||||||
image: docker.io/library/caddy:2-alpine
|
image: caddy:2-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
|
@ -140,64 +89,19 @@ services:
|
||||||
- ../proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
- ../proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- caddy-data:/data
|
- caddy-data:/data
|
||||||
- caddy-config:/config
|
- caddy-config:/config
|
||||||
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile --resume
|
||||||
networks:
|
networks:
|
||||||
- hiy-net
|
- hiy-net
|
||||||
- default
|
- default
|
||||||
|
|
||||||
# ── Uptime / health checks ────────────────────────────────────────────────
|
|
||||||
# Enable with: podman compose --profile monitoring up -d
|
|
||||||
gatus:
|
|
||||||
profiles: [monitoring]
|
|
||||||
image: docker.io/twinproduction/gatus:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- ./gatus.yml:/config/config.yaml:ro
|
|
||||||
networks:
|
|
||||||
- hiy-net
|
|
||||||
|
|
||||||
# ── Host metrics (rootful Podman / Docker only) ───────────────────────────
|
|
||||||
# On rootless Podman some host mounts may be unavailable; comment out if so.
|
|
||||||
netdata:
|
|
||||||
profiles: [monitoring]
|
|
||||||
image: docker.io/netdata/netdata:stable
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "19999:19999"
|
|
||||||
pid: host
|
|
||||||
cap_add:
|
|
||||||
- SYS_PTRACE
|
|
||||||
- SYS_ADMIN
|
|
||||||
security_opt:
|
|
||||||
- apparmor:unconfined
|
|
||||||
volumes:
|
|
||||||
- netdata-config:/etc/netdata
|
|
||||||
- netdata-lib:/var/lib/netdata
|
|
||||||
- netdata-cache:/var/cache/netdata
|
|
||||||
- /etc/os-release:/host/etc/os-release:ro
|
|
||||||
- /etc/passwd:/host/etc/passwd:ro
|
|
||||||
- /etc/group:/host/etc/group:ro
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
networks:
|
|
||||||
- hiy-net
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
hiy-net:
|
hiy-net:
|
||||||
name: hiy-net
|
name: hiy-net
|
||||||
# External so deployed app containers can join it.
|
# External so deployed app containers can join it.
|
||||||
external: false
|
external: false
|
||||||
default: {}
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
hiy-data:
|
hiy-data:
|
||||||
forgejo-data:
|
|
||||||
act_runner_data:
|
|
||||||
caddy-data:
|
caddy-data:
|
||||||
caddy-config:
|
caddy-config:
|
||||||
hiy-pg-data:
|
hiy-pg-data:
|
||||||
netdata-config:
|
|
||||||
netdata-lib:
|
|
||||||
netdata-cache:
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# Gatus uptime / health check configuration for HIY.
|
|
||||||
# Docs: https://github.com/TwiN/gatus
|
|
||||||
|
|
||||||
web:
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
# In-memory storage — no persistence needed for uptime checks.
|
|
||||||
storage:
|
|
||||||
type: memory
|
|
||||||
|
|
||||||
# Alert via email when an endpoint is down (optional — remove if not needed).
|
|
||||||
# alerting:
|
|
||||||
# email:
|
|
||||||
# from: gatus@yourdomain.com
|
|
||||||
# username: gatus@yourdomain.com
|
|
||||||
# password: ${EMAIL_PASSWORD}
|
|
||||||
# host: smtp.yourdomain.com
|
|
||||||
# port: 587
|
|
||||||
# to: you@yourdomain.com
|
|
||||||
|
|
||||||
endpoints:
|
|
||||||
- name: HIY Dashboard
|
|
||||||
url: http://server:3000/api/status
|
|
||||||
interval: 1m
|
|
||||||
conditions:
|
|
||||||
- "[STATUS] == 200"
|
|
||||||
alerts:
|
|
||||||
- type: email
|
|
||||||
description: HIY dashboard is unreachable
|
|
||||||
send-on-resolved: true
|
|
||||||
|
|
||||||
# Add an entry per deployed app:
|
|
||||||
#
|
|
||||||
# - name: my-app
|
|
||||||
# url: http://my-app:3001/health
|
|
||||||
# interval: 1m
|
|
||||||
# conditions:
|
|
||||||
# - "[STATUS] == 200"
|
|
||||||
# - "[RESPONSE_TIME] < 500"
|
|
||||||
142
infra/install.sh
142
infra/install.sh
|
|
@ -1,142 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# install.sh — one-time setup for a fresh Raspberry Pi.
|
|
||||||
#
|
|
||||||
# Run this once after cloning the repo:
|
|
||||||
# cd ~/Hostityourself && ./infra/install.sh
|
|
||||||
#
|
|
||||||
# What it does:
|
|
||||||
# 1. Installs system packages (podman, aardvark-dns, sqlite3, git, uidmap)
|
|
||||||
# 2. Installs podman-compose (via pip, into ~/.local/bin)
|
|
||||||
# 3. Installs rclone (for off-site backups — optional)
|
|
||||||
# 4. Creates .env from the template and prompts for required values
|
|
||||||
# 5. Runs infra/start.sh to build and launch the stack
|
|
||||||
#
|
|
||||||
# Safe to re-run — all steps are idempotent.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
log() { echo; echo "▶ $*"; }
|
|
||||||
info() { echo " $*"; }
|
|
||||||
ok() { echo " ✓ $*"; }
|
|
||||||
|
|
||||||
echo "╔══════════════════════════════════════════╗"
|
|
||||||
echo "║ HostItYourself — installer ║"
|
|
||||||
echo "╚══════════════════════════════════════════╝"
|
|
||||||
|
|
||||||
# ── 1. System packages ─────────────────────────────────────────────────────────
|
|
||||||
log "Installing system packages…"
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y \
|
|
||||||
podman \
|
|
||||||
aardvark-dns \
|
|
||||||
sqlite3 \
|
|
||||||
git \
|
|
||||||
uidmap \
|
|
||||||
python3-pip \
|
|
||||||
python3-venv \
|
|
||||||
curl
|
|
||||||
ok "System packages installed."
|
|
||||||
|
|
||||||
# ── 2. podman-compose ──────────────────────────────────────────────────────────
|
|
||||||
log "Checking podman-compose…"
|
|
||||||
if command -v podman-compose &>/dev/null; then
|
|
||||||
ok "podman-compose already installed ($(podman-compose --version 2>&1 | head -1))."
|
|
||||||
else
|
|
||||||
info "Installing podman-compose via pip…"
|
|
||||||
pip3 install --user podman-compose
|
|
||||||
ok "podman-compose installed to ~/.local/bin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure ~/.local/bin is in PATH for this session and future logins.
|
|
||||||
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
fi
|
|
||||||
PROFILE="$HOME/.bashrc"
|
|
||||||
if ! grep -q '\.local/bin' "$PROFILE" 2>/dev/null; then
|
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$PROFILE"
|
|
||||||
info "Added ~/.local/bin to PATH in $PROFILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. rclone (optional) ───────────────────────────────────────────────────────
|
|
||||||
log "rclone (used for off-site backups)…"
|
|
||||||
if command -v rclone &>/dev/null; then
|
|
||||||
ok "rclone already installed ($(rclone --version 2>&1 | head -1))."
|
|
||||||
else
|
|
||||||
read -r -p " Install rclone? [y/N] " _RCLONE
|
|
||||||
if [[ "${_RCLONE,,}" == "y" ]]; then
|
|
||||||
curl -fsSL https://rclone.org/install.sh | sudo bash
|
|
||||||
ok "rclone installed."
|
|
||||||
info "Configure a remote later with: rclone config"
|
|
||||||
info "Then set HIY_BACKUP_REMOTE in .env"
|
|
||||||
else
|
|
||||||
info "Skipped. Install later with: curl https://rclone.org/install.sh | sudo bash"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. .env setup ──────────────────────────────────────────────────────────────
|
|
||||||
log "Setting up .env…"
|
|
||||||
ENV_FILE="$REPO_ROOT/.env"
|
|
||||||
ENV_EXAMPLE="$SCRIPT_DIR/.env.example"
|
|
||||||
|
|
||||||
if [ -f "$ENV_FILE" ]; then
|
|
||||||
ok ".env already exists — skipping (edit manually if needed)."
|
|
||||||
else
|
|
||||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
|
||||||
info "Created .env from template. Filling in required values…"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Helper: prompt for a value and write it into .env.
|
|
||||||
set_env() {
|
|
||||||
local key="$1" prompt="$2" default="$3" secret="${4:-}"
|
|
||||||
local current
|
|
||||||
current=$(grep "^${key}=" "$ENV_FILE" | cut -d= -f2- || echo "")
|
|
||||||
if [ -z "$current" ] || [ "$current" = "changeme" ] || [ "$current" = "" ]; then
|
|
||||||
if [ -n "$secret" ]; then
|
|
||||||
read -r -s -p " ${prompt} [${default}]: " _VAL; echo
|
|
||||||
else
|
|
||||||
read -r -p " ${prompt} [${default}]: " _VAL
|
|
||||||
fi
|
|
||||||
_VAL="${_VAL:-$default}"
|
|
||||||
# Replace the line in .env (works on both macOS and Linux).
|
|
||||||
sed -i "s|^${key}=.*|${key}=${_VAL}|" "$ENV_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env "DOMAIN_SUFFIX" "Your domain (e.g. example.com)" "yourdomain.com"
|
|
||||||
set_env "ACME_EMAIL" "Email for Let's Encrypt notices" ""
|
|
||||||
set_env "HIY_ADMIN_USER" "Dashboard admin username" "admin"
|
|
||||||
set_env "HIY_ADMIN_PASS" "Dashboard admin password" "$(openssl rand -hex 12)" "secret"
|
|
||||||
set_env "POSTGRES_PASSWORD" "Postgres admin password" "$(openssl rand -hex 16)" "secret"
|
|
||||||
set_env "FORGEJO_DB_PASSWORD" "Forgejo DB password" "$(openssl rand -hex 16)" "secret"
|
|
||||||
set_env "FORGEJO_DOMAIN" "Forgejo domain (e.g. git.example.com)" "git.yourdomain.com"
|
|
||||||
|
|
||||||
echo
|
|
||||||
ok ".env written to $ENV_FILE"
|
|
||||||
info "Review it with: cat $ENV_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 5. Git remote check ────────────────────────────────────────────────────────
|
|
||||||
log "Checking git remote…"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
|
|
||||||
if [ -n "$REMOTE_URL" ]; then
|
|
||||||
ok "Git remote: $REMOTE_URL"
|
|
||||||
else
|
|
||||||
info "No git remote configured — auto-update will not work."
|
|
||||||
info "Set one with: git remote add origin <url>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure the tracking branch is set so auto-update.sh can compare commits.
|
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' &>/dev/null; then
|
|
||||||
info "Setting upstream tracking branch…"
|
|
||||||
git branch --set-upstream-to="origin/$BRANCH" "$BRANCH" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. Launch the stack ────────────────────────────────────────────────────────
|
|
||||||
log "Running start.sh to build and launch the stack…"
|
|
||||||
echo
|
|
||||||
exec "$SCRIPT_DIR/start.sh"
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Create a dedicated database and user for Forgejo.
|
|
||||||
# Runs once when the Postgres container is first initialised.
|
|
||||||
# FORGEJO_DB_PASSWORD must be set in the environment (via docker-compose.yml).
|
|
||||||
set -e
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
|
|
||||||
CREATE USER forgejo WITH PASSWORD '${FORGEJO_DB_PASSWORD}';
|
|
||||||
CREATE DATABASE forgejo OWNER forgejo;
|
|
||||||
EOSQL
|
|
||||||
143
infra/restore.sh
143
infra/restore.sh
|
|
@ -1,143 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# HIY restore script
|
|
||||||
#
|
|
||||||
# Restores a backup archive produced by infra/backup.sh.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./infra/restore.sh /path/to/hiy-backup-20260101-030000.tar.gz
|
|
||||||
#
|
|
||||||
# What is restored:
|
|
||||||
# 1. SQLite database (hiy.db)
|
|
||||||
# 2. Env files and git repos
|
|
||||||
# 3. Postgres databases (pg_dumpall dump)
|
|
||||||
# 4. Forgejo data volume
|
|
||||||
# 5. Caddy TLS certificates
|
|
||||||
# 6. .env file (optional — skipped if already present unless --force is passed)
|
|
||||||
#
|
|
||||||
# ⚠ Run this with the stack STOPPED, then bring it back up afterwards:
|
|
||||||
# podman compose -f infra/docker-compose.yml down
|
|
||||||
# ./infra/restore.sh hiy-backup-*.tar.gz
|
|
||||||
# podman compose -f infra/docker-compose.yml up -d
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ARCHIVE="${1:-}"
|
|
||||||
FORCE="${2:-}"
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
ENV_FILE="${SCRIPT_DIR}/../.env"
|
|
||||||
HIY_DATA_DIR="${HIY_DATA_DIR:-/data}"
|
|
||||||
|
|
||||||
log() { echo "[hiy-restore] $(date '+%H:%M:%S') $*"; }
|
|
||||||
die() { log "ERROR: $*"; exit 1; }
|
|
||||||
|
|
||||||
# ── Validate ───────────────────────────────────────────────────────────────────
|
|
||||||
[ -z "${ARCHIVE}" ] && die "Usage: $0 <archive.tar.gz> [--force]"
|
|
||||||
[ -f "${ARCHIVE}" ] || die "Archive not found: ${ARCHIVE}"
|
|
||||||
|
|
||||||
WORK_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "${WORK_DIR}"' EXIT
|
|
||||||
|
|
||||||
log "=== HIY Restore ==="
|
|
||||||
log "Archive : ${ARCHIVE}"
|
|
||||||
log "Work dir: ${WORK_DIR}"
|
|
||||||
|
|
||||||
log "Extracting archive…"
|
|
||||||
tar -xzf "${ARCHIVE}" -C "${WORK_DIR}"
|
|
||||||
|
|
||||||
# ── Helper: find a running container by compose service label ──────────────────
|
|
||||||
find_container() {
|
|
||||||
local service="$1"
|
|
||||||
podman ps --filter "label=com.docker.compose.service=${service}" \
|
|
||||||
--format '{{.Names}}' | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 1. .env file ───────────────────────────────────────────────────────────────
|
|
||||||
log "--- .env ---"
|
|
||||||
if [ -f "${WORK_DIR}/dot-env" ]; then
|
|
||||||
if [ -f "${ENV_FILE}" ] && [ "${FORCE}" != "--force" ]; then
|
|
||||||
log "SKIP: ${ENV_FILE} already exists (pass --force to overwrite)"
|
|
||||||
else
|
|
||||||
cp "${WORK_DIR}/dot-env" "${ENV_FILE}"
|
|
||||||
log "Restored .env to ${ENV_FILE}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "No .env in archive — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 2. SQLite ──────────────────────────────────────────────────────────────────
|
|
||||||
log "--- SQLite ---"
|
|
||||||
if [ -f "${WORK_DIR}/hiy.sql" ]; then
|
|
||||||
DB_PATH="${HIY_DATA_DIR}/hiy.db"
|
|
||||||
mkdir -p "$(dirname "${DB_PATH}")"
|
|
||||||
if [ -f "${DB_PATH}" ]; then
|
|
||||||
log "Moving existing hiy.db to hiy.db.bak…"
|
|
||||||
mv "${DB_PATH}" "${DB_PATH}.bak"
|
|
||||||
fi
|
|
||||||
log "Restoring hiy.db…"
|
|
||||||
sqlite3 "${DB_PATH}" < "${WORK_DIR}/hiy.sql"
|
|
||||||
log "SQLite restored."
|
|
||||||
else
|
|
||||||
log "No hiy.sql in archive — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. Env files & git repos ───────────────────────────────────────────────────
|
|
||||||
log "--- Env files ---"
|
|
||||||
if [ -f "${WORK_DIR}/envs.tar.gz" ]; then
|
|
||||||
log "Restoring envs/…"
|
|
||||||
tar -xzf "${WORK_DIR}/envs.tar.gz" -C "${HIY_DATA_DIR}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "--- Git repos ---"
|
|
||||||
if [ -f "${WORK_DIR}/repos.tar.gz" ]; then
|
|
||||||
log "Restoring repos/…"
|
|
||||||
tar -xzf "${WORK_DIR}/repos.tar.gz" -C "${HIY_DATA_DIR}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. Postgres ────────────────────────────────────────────────────────────────
|
|
||||||
log "--- Postgres ---"
|
|
||||||
if [ -f "${WORK_DIR}/postgres.sql" ]; then
|
|
||||||
PG_CTR=$(find_container postgres)
|
|
||||||
if [ -n "${PG_CTR}" ]; then
|
|
||||||
log "Restoring Postgres via container ${PG_CTR}…"
|
|
||||||
# Drop existing connections then restore.
|
|
||||||
podman exec -i "${PG_CTR}" psql -U hiy_admin -d postgres \
|
|
||||||
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IN ('hiy','forgejo') AND pid <> pg_backend_pid();" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
podman exec -i "${PG_CTR}" psql -U hiy_admin -d postgres \
|
|
||||||
< "${WORK_DIR}/postgres.sql"
|
|
||||||
log "Postgres restored."
|
|
||||||
else
|
|
||||||
log "WARNING: postgres container not running"
|
|
||||||
log " Start Postgres first, then run:"
|
|
||||||
log " podman exec -i <postgres_container> psql -U hiy_admin -d postgres < ${WORK_DIR}/postgres.sql"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "No postgres.sql in archive — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 5. Forgejo data volume ─────────────────────────────────────────────────────
|
|
||||||
log "--- Forgejo volume ---"
|
|
||||||
if [ -f "${WORK_DIR}/forgejo-data.tar" ]; then
|
|
||||||
log "Importing forgejo-data volume…"
|
|
||||||
podman volume exists forgejo-data 2>/dev/null || podman volume create forgejo-data
|
|
||||||
podman volume import forgejo-data "${WORK_DIR}/forgejo-data.tar"
|
|
||||||
log "forgejo-data restored."
|
|
||||||
else
|
|
||||||
log "No forgejo-data.tar in archive — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. Caddy TLS certificates ──────────────────────────────────────────────────
|
|
||||||
log "--- Caddy volume ---"
|
|
||||||
if [ -f "${WORK_DIR}/caddy-data.tar" ]; then
|
|
||||||
log "Importing caddy-data volume…"
|
|
||||||
podman volume exists caddy-data 2>/dev/null || podman volume create caddy-data
|
|
||||||
podman volume import caddy-data "${WORK_DIR}/caddy-data.tar"
|
|
||||||
log "caddy-data restored."
|
|
||||||
else
|
|
||||||
log "No caddy-data.tar in archive — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "=== Restore complete ==="
|
|
||||||
log "Bring the stack back up with:"
|
|
||||||
log " podman compose -f ${SCRIPT_DIR}/docker-compose.yml up -d"
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# runner-entrypoint.sh — register the Forgejo runner on first start, then run the daemon.
|
|
||||||
#
|
|
||||||
# On first run (no /data/.runner file) it calls create-runner-file to register
|
|
||||||
# with the Forgejo instance using FORGEJO_RUNNER_TOKEN. On subsequent starts it
|
|
||||||
# goes straight to the daemon.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CONFIG=/data/.runner
|
|
||||||
|
|
||||||
if [ ! -f "$CONFIG" ]; then
|
|
||||||
echo "[runner] No registration found — registering with Forgejo…"
|
|
||||||
forgejo-runner register \
|
|
||||||
--instance "${FORGEJO_INSTANCE_URL}" \
|
|
||||||
--token "${FORGEJO_RUNNER_TOKEN}" \
|
|
||||||
--name "${FORGEJO_RUNNER_NAME:-hiy-runner}" \
|
|
||||||
--labels "ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://node:20-bookworm" \
|
|
||||||
--no-interactive
|
|
||||||
echo "[runner] Registration complete."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[runner] Starting daemon…"
|
|
||||||
exec forgejo-runner daemon --config "$CONFIG"
|
|
||||||
|
|
@ -20,10 +20,45 @@ if [ -z "$DOMAIN_SUFFIX" ] || [ "$DOMAIN_SUFFIX" = "localhost" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$ACME_EMAIL" ]; then
|
if [ -z "$ACME_EMAIL" ]; then
|
||||||
echo "[hiy] ACME_EMAIL not set — Caddy will use its internal CA (self-signed)."
|
echo "ERROR: Set ACME_EMAIL in infra/.env (required for Let's Encrypt)"
|
||||||
echo "[hiy] For a public domain with Let's Encrypt, set ACME_EMAIL in infra/.env"
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Generate production caddy.json ─────────────────────────────────────────────
|
||||||
|
# Writes TLS-enabled config using Let's Encrypt (no Cloudflare required).
|
||||||
|
# Caddy will use the HTTP-01 challenge (port 80) or TLS-ALPN-01 (port 443).
|
||||||
|
cat > "$SCRIPT_DIR/../proxy/caddy.json" <<EOF
|
||||||
|
{
|
||||||
|
"admin": { "listen": "0.0.0.0:2019" },
|
||||||
|
"apps": {
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [{
|
||||||
|
"subjects": ["${DOMAIN_SUFFIX}"],
|
||||||
|
"issuers": [{"module": "acme", "email": "${ACME_EMAIL}"}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"hiy": {
|
||||||
|
"listen": [":80", ":443"],
|
||||||
|
"automatic_https": {},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["${DOMAIN_SUFFIX}"]}],
|
||||||
|
"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "server:3000"}]}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[hiy] Generated proxy/caddy.json for ${DOMAIN_SUFFIX}"
|
||||||
|
|
||||||
# ── Ensure cgroup swap accounting is enabled (required by runc/Podman) ────────
|
# ── Ensure cgroup swap accounting is enabled (required by runc/Podman) ────────
|
||||||
# runc always writes memory.swap.max when the memory cgroup controller is
|
# runc always writes memory.swap.max when the memory cgroup controller is
|
||||||
# present. On Raspberry Pi OS swap accounting is disabled by default, so that
|
# present. On Raspberry Pi OS swap accounting is disabled by default, so that
|
||||||
|
|
@ -171,7 +206,6 @@ Wants=network-online.target
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/$(id -un)/.local/bin
|
|
||||||
ExecStart=${SCRIPT_DIR}/boot.sh
|
ExecStart=${SCRIPT_DIR}/boot.sh
|
||||||
ExecStop=podman compose --env-file ${REPO_ROOT}/.env -f ${SCRIPT_DIR}/docker-compose.yml down
|
ExecStop=podman compose --env-file ${REPO_ROOT}/.env -f ${SCRIPT_DIR}/docker-compose.yml down
|
||||||
|
|
||||||
|
|
@ -183,37 +217,3 @@ systemctl --user daemon-reload
|
||||||
systemctl --user enable hiy.service
|
systemctl --user enable hiy.service
|
||||||
loginctl enable-linger "$(id -un)" 2>/dev/null || true
|
loginctl enable-linger "$(id -un)" 2>/dev/null || true
|
||||||
echo "[hiy] Boot service installed: systemctl --user status hiy.service"
|
echo "[hiy] Boot service installed: systemctl --user status hiy.service"
|
||||||
|
|
||||||
# ── Install systemd timer for auto-update ─────────────────────────────────────
|
|
||||||
UPDATE_SERVICE="$SERVICE_DIR/hiy-update.service"
|
|
||||||
UPDATE_TIMER="$SERVICE_DIR/hiy-update.timer"
|
|
||||||
|
|
||||||
cat > "$UPDATE_SERVICE" <<UNIT
|
|
||||||
[Unit]
|
|
||||||
Description=HIY auto-update (git pull + restart changed services)
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/$(id -un)/.local/bin
|
|
||||||
ExecStart=${SCRIPT_DIR}/auto-update.sh
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
UNIT
|
|
||||||
|
|
||||||
cat > "$UPDATE_TIMER" <<UNIT
|
|
||||||
[Unit]
|
|
||||||
Description=HIY auto-update every 5 minutes
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=2min
|
|
||||||
OnUnitActiveSec=5min
|
|
||||||
Unit=hiy-update.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
UNIT
|
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user enable --now hiy-update.timer
|
|
||||||
echo "[hiy] Auto-update timer installed: systemctl --user status hiy-update.timer"
|
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
# HIY dashboard — served at your root domain.
|
# HIY dashboard — served at your root domain.
|
||||||
# TLS behaviour:
|
|
||||||
# ACME_EMAIL set → Caddy requests a Let's Encrypt cert (production)
|
|
||||||
# ACME_EMAIL unset → Caddy uses its built-in internal CA (local / LAN domains)
|
|
||||||
{$DOMAIN_SUFFIX:localhost} {
|
{$DOMAIN_SUFFIX:localhost} {
|
||||||
tls {$ACME_EMAIL:internal}
|
|
||||||
reverse_proxy server:3000
|
reverse_proxy server:3000
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Static services (not managed by HIY) ──────────────────────────────────────
|
|
||||||
|
|
||||||
# Set FORGEJO_DOMAIN in .env (e.g. git.yourdomain.com). Falls back to a
|
|
||||||
# non-routable placeholder so Caddy starts cleanly even if Forgejo isn't used.
|
|
||||||
{$FORGEJO_DOMAIN:forgejo.localhost} {
|
|
||||||
tls {$ACME_EMAIL:internal}
|
|
||||||
reverse_proxy forgejo:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deployed apps are added here dynamically by hiy-server via the Caddy API.
|
# Deployed apps are added here dynamically by hiy-server via the Caddy API.
|
||||||
# Each entry looks like:
|
# Each entry looks like:
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
bcrypt = "0.15"
|
bcrypt = "0.15"
|
||||||
aes-gcm = "0.10"
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,10 @@ async fn run_build(state: &AppState, deploy_id: &str) -> anyhow::Result<()> {
|
||||||
.fetch_all(&state.db)
|
.fetch_all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut env_content = String::new();
|
let env_content: String = env_vars
|
||||||
for e in &env_vars {
|
.iter()
|
||||||
let plain = crate::crypto::decrypt(&e.value)
|
.map(|e| format!("{}={}\n", e.key, e.value))
|
||||||
.unwrap_or_else(|err| {
|
.collect();
|
||||||
tracing::warn!("Could not decrypt env var {}: {} — using raw value", e.key, err);
|
|
||||||
e.value.clone()
|
|
||||||
});
|
|
||||||
env_content.push_str(&format!("{}={}\n", e.key, plain));
|
|
||||||
}
|
|
||||||
std::fs::write(&env_file, env_content)?;
|
std::fs::write(&env_file, env_content)?;
|
||||||
|
|
||||||
// Mark as building.
|
// Mark as building.
|
||||||
|
|
@ -133,30 +128,16 @@ async fn run_build(state: &AppState, deploy_id: &str) -> anyhow::Result<()> {
|
||||||
let domain_suffix = std::env::var("DOMAIN_SUFFIX").unwrap_or_else(|_| "localhost".into());
|
let domain_suffix = std::env::var("DOMAIN_SUFFIX").unwrap_or_else(|_| "localhost".into());
|
||||||
let caddy_api_url = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://localhost:2019".into());
|
let caddy_api_url = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://localhost:2019".into());
|
||||||
|
|
||||||
let mut cmd = Command::new("bash");
|
let mut child = Command::new("bash")
|
||||||
cmd.arg(&build_script)
|
.arg(&build_script)
|
||||||
.env("APP_ID", &app.id)
|
.env("APP_ID", &app.id)
|
||||||
.env("APP_NAME", &app.name)
|
.env("APP_NAME", &app.name)
|
||||||
.env("REPO_URL", &repo_url);
|
.env("REPO_URL", &repo_url)
|
||||||
|
|
||||||
// Decrypt the git token (if any) and pass it separately so build.sh can
|
|
||||||
// inject it into the clone URL without it appearing in REPO_URL or logs.
|
|
||||||
if let Some(enc) = &app.git_token {
|
|
||||||
match crate::crypto::decrypt(enc) {
|
|
||||||
Ok(tok) => { cmd.env("GIT_TOKEN", tok); }
|
|
||||||
Err(e) => tracing::warn!("Could not decrypt git_token for {}: {}", app.id, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut child = cmd
|
|
||||||
.env("BRANCH", &app.branch)
|
.env("BRANCH", &app.branch)
|
||||||
.env("PORT", app.port.to_string())
|
.env("PORT", app.port.to_string())
|
||||||
.env("ENV_FILE", &env_file)
|
.env("ENV_FILE", &env_file)
|
||||||
.env("SHA", deploy.sha.as_deref().unwrap_or(""))
|
.env("SHA", deploy.sha.as_deref().unwrap_or(""))
|
||||||
.env("BUILD_DIR", &build_dir)
|
.env("BUILD_DIR", &build_dir)
|
||||||
.env("MEMORY_LIMIT", &app.memory_limit)
|
|
||||||
.env("CPU_LIMIT", &app.cpu_limit)
|
|
||||||
.env("IS_PUBLIC", if app.is_public != 0 { "1" } else { "0" })
|
|
||||||
.env("DOMAIN_SUFFIX", &domain_suffix)
|
.env("DOMAIN_SUFFIX", &domain_suffix)
|
||||||
.env("CADDY_API_URL", &caddy_api_url)
|
.env("CADDY_API_URL", &caddy_api_url)
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/// AES-256-GCM envelope encryption for values stored at rest.
|
|
||||||
///
|
|
||||||
/// Encrypted blobs are prefixed with `enc:v1:` so plaintext values written
|
|
||||||
/// before encryption was enabled are transparently passed through on decrypt.
|
|
||||||
///
|
|
||||||
/// Key derivation: SHA-256 of `HIY_SECRET_KEY` env var. If the var is
|
|
||||||
/// absent a hard-coded default is used and a warning is logged once.
|
|
||||||
use aes_gcm::{
|
|
||||||
aead::{Aead, AeadCore, KeyInit, OsRng},
|
|
||||||
Aes256Gcm, Key, Nonce,
|
|
||||||
};
|
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
const PREFIX: &str = "enc:v1:";
|
|
||||||
|
|
||||||
fn key_bytes() -> [u8; 32] {
|
|
||||||
let secret = std::env::var("HIY_SECRET_KEY").unwrap_or_else(|_| {
|
|
||||||
tracing::warn!(
|
|
||||||
"HIY_SECRET_KEY is not set — env vars are encrypted with the default insecure key. \
|
|
||||||
Set HIY_SECRET_KEY in .env to a random 32+ char string."
|
|
||||||
);
|
|
||||||
"hiy-default-insecure-key-please-change-me".into()
|
|
||||||
});
|
|
||||||
Sha256::digest(secret.as_bytes()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encrypt a plaintext value and return `enc:v1:<b64(nonce||ciphertext)>`.
|
|
||||||
pub fn encrypt(plaintext: &str) -> anyhow::Result<String> {
|
|
||||||
let kb = key_bytes();
|
|
||||||
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&kb));
|
|
||||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
|
||||||
let ct = cipher
|
|
||||||
.encrypt(&nonce, plaintext.as_bytes())
|
|
||||||
.map_err(|e| anyhow::anyhow!("encrypt: {}", e))?;
|
|
||||||
let mut blob = nonce.to_vec();
|
|
||||||
blob.extend_from_slice(&ct);
|
|
||||||
Ok(format!("{}{}", PREFIX, STANDARD.encode(&blob)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt an `enc:v1:…` value. Non-prefixed strings are returned as-is
|
|
||||||
/// (transparent migration path for pre-encryption data).
|
|
||||||
pub fn decrypt(value: &str) -> anyhow::Result<String> {
|
|
||||||
if !value.starts_with(PREFIX) {
|
|
||||||
return Ok(value.to_string());
|
|
||||||
}
|
|
||||||
let blob = STANDARD
|
|
||||||
.decode(&value[PREFIX.len()..])
|
|
||||||
.map_err(|e| anyhow::anyhow!("base64: {}", e))?;
|
|
||||||
if blob.len() < 12 {
|
|
||||||
anyhow::bail!("ciphertext too short");
|
|
||||||
}
|
|
||||||
let (nonce_bytes, ct) = blob.split_at(12);
|
|
||||||
let kb = key_bytes();
|
|
||||||
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&kb));
|
|
||||||
let plain = cipher
|
|
||||||
.decrypt(Nonce::from_slice(nonce_bytes), ct)
|
|
||||||
.map_err(|e| anyhow::anyhow!("decrypt: {}", e))?;
|
|
||||||
String::from_utf8(plain).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
@ -101,16 +101,6 @@ pub async fn migrate(pool: &DbPool) -> anyhow::Result<()> {
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Idempotent column additions for existing databases (SQLite ignores "column exists" errors).
|
|
||||||
let _ = sqlx::query("ALTER TABLE apps ADD COLUMN memory_limit TEXT NOT NULL DEFAULT '512m'")
|
|
||||||
.execute(pool).await;
|
|
||||||
let _ = sqlx::query("ALTER TABLE apps ADD COLUMN cpu_limit TEXT NOT NULL DEFAULT '0.5'")
|
|
||||||
.execute(pool).await;
|
|
||||||
let _ = sqlx::query("ALTER TABLE apps ADD COLUMN git_token TEXT")
|
|
||||||
.execute(pool).await;
|
|
||||||
let _ = sqlx::query("ALTER TABLE apps ADD COLUMN is_public INTEGER NOT NULL DEFAULT 0")
|
|
||||||
.execute(pool).await;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"CREATE TABLE IF NOT EXISTS databases (
|
r#"CREATE TABLE IF NOT EXISTS databases (
|
||||||
app_id TEXT PRIMARY KEY REFERENCES apps(id) ON DELETE CASCADE,
|
app_id TEXT PRIMARY KEY REFERENCES apps(id) ON DELETE CASCADE,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod crypto;
|
|
||||||
mod db;
|
mod db;
|
||||||
mod models;
|
mod models;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
@ -154,20 +153,6 @@ async fn main() -> anyhow::Result<()> {
|
||||||
builder::build_worker(worker_state).await;
|
builder::build_worker(worker_state).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-register all app Caddy routes from the DB on startup.
|
|
||||||
// Caddy no longer uses --resume, so routes must be restored each time the
|
|
||||||
// stack restarts (ensures Caddyfile changes are always picked up).
|
|
||||||
let restore_db = state.db.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
routes::apps::restore_caddy_routes(&restore_db).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart any app containers that are stopped (e.g. after a host reboot).
|
|
||||||
let containers_db = state.db.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
routes::apps::restore_app_containers(&containers_db).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Protected routes (admin login required) ───────────────────────────────
|
// ── Protected routes (admin login required) ───────────────────────────────
|
||||||
let protected = Router::new()
|
let protected = Router::new()
|
||||||
.route("/", get(routes::ui::index))
|
.route("/", get(routes::ui::index))
|
||||||
|
|
@ -178,7 +163,6 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.route("/api/apps", get(routes::apps::list).post(routes::apps::create))
|
.route("/api/apps", get(routes::apps::list).post(routes::apps::create))
|
||||||
.route("/api/apps/:id", get(routes::apps::get_one)
|
.route("/api/apps/:id", get(routes::apps::get_one)
|
||||||
.put(routes::apps::update)
|
.put(routes::apps::update)
|
||||||
.patch(routes::apps::update)
|
|
||||||
.delete(routes::apps::delete))
|
.delete(routes::apps::delete))
|
||||||
.route("/api/apps/:id/stop", post(routes::apps::stop))
|
.route("/api/apps/:id/stop", post(routes::apps::stop))
|
||||||
.route("/api/apps/:id/restart", post(routes::apps::restart))
|
.route("/api/apps/:id/restart", post(routes::apps::restart))
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,8 @@ pub struct App {
|
||||||
pub branch: String,
|
pub branch: String,
|
||||||
pub port: i64,
|
pub port: i64,
|
||||||
pub webhook_secret: String,
|
pub webhook_secret: String,
|
||||||
pub memory_limit: String,
|
|
||||||
pub cpu_limit: String,
|
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
/// Encrypted git token for cloning private repos. Never serialised to API responses.
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub git_token: Option<String>,
|
|
||||||
pub is_public: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -24,9 +18,6 @@ pub struct CreateApp {
|
||||||
pub repo_url: Option<String>,
|
pub repo_url: Option<String>,
|
||||||
pub branch: Option<String>,
|
pub branch: Option<String>,
|
||||||
pub port: i64,
|
pub port: i64,
|
||||||
pub memory_limit: Option<String>,
|
|
||||||
pub cpu_limit: Option<String>,
|
|
||||||
pub git_token: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -34,10 +25,6 @@ pub struct UpdateApp {
|
||||||
pub repo_url: Option<String>,
|
pub repo_url: Option<String>,
|
||||||
pub branch: Option<String>,
|
pub branch: Option<String>,
|
||||||
pub port: Option<i64>,
|
pub port: Option<i64>,
|
||||||
pub memory_limit: Option<String>,
|
|
||||||
pub cpu_limit: Option<String>,
|
|
||||||
pub git_token: Option<String>,
|
|
||||||
pub is_public: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
|
|
||||||
|
|
@ -12,187 +12,6 @@ use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Build the Caddy route JSON for an app.
|
|
||||||
/// Public apps get a plain reverse_proxy; private apps get forward_auth via HIY.
|
|
||||||
fn caddy_route(app_host: &str, upstream: &str, is_public: bool) -> serde_json::Value {
|
|
||||||
if is_public {
|
|
||||||
serde_json::json!({
|
|
||||||
"match": [{"host": [app_host]}],
|
|
||||||
"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": upstream}]}]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
serde_json::json!({
|
|
||||||
"match": [{"host": [app_host]}],
|
|
||||||
"handle": [{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [{
|
|
||||||
"handle": [{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"rewrite": {"method": "GET", "uri": "/auth/verify"},
|
|
||||||
"headers": {"request": {"set": {
|
|
||||||
"X-Forwarded-Method": ["{http.request.method}"],
|
|
||||||
"X-Forwarded-Uri": ["{http.request.uri}"],
|
|
||||||
"X-Forwarded-Host": ["{http.request.host}"],
|
|
||||||
"X-Forwarded-Proto": ["{http.request.scheme}"]
|
|
||||||
}}},
|
|
||||||
"upstreams": [{"dial": "server:3000"}],
|
|
||||||
"handle_response": [{
|
|
||||||
"match": {"status_code": [2]},
|
|
||||||
"routes": [{"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": upstream}]}]}]
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-register every app's Caddy route from the database.
|
|
||||||
/// Called at startup so that removing `--resume` from Caddy doesn't lose
|
|
||||||
/// routes when the stack restarts.
|
|
||||||
pub async fn restore_caddy_routes(db: &crate::DbPool) {
|
|
||||||
// Give Caddy a moment to finish loading the Caddyfile before we PATCH it.
|
|
||||||
let caddy_api = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://caddy:2019".into());
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
for attempt in 1..=10u32 {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
||||||
if client.get(format!("{}/config/", caddy_api)).send().await.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tracing::info!("restore_caddy_routes: waiting for Caddy ({}/10)…", attempt);
|
|
||||||
}
|
|
||||||
|
|
||||||
let apps = match sqlx::query_as::<_, crate::models::App>("SELECT * FROM apps")
|
|
||||||
.fetch_all(db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(a) => a,
|
|
||||||
Err(e) => { tracing::error!("restore_caddy_routes: DB error: {}", e); return; }
|
|
||||||
};
|
|
||||||
|
|
||||||
for app in &apps {
|
|
||||||
push_visibility_to_caddy(&app.id, app.port, app.is_public != 0).await;
|
|
||||||
}
|
|
||||||
tracing::info!("restore_caddy_routes: registered {} app routes", apps.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// On startup, ensure every app that had a successful deploy is actually running.
|
|
||||||
/// If the host rebooted, containers will be in "exited" state — start them.
|
|
||||||
/// If a container is missing entirely, log a warning (we don't rebuild automatically).
|
|
||||||
pub async fn restore_app_containers(db: &crate::DbPool) {
|
|
||||||
let apps = match sqlx::query_as::<_, crate::models::App>("SELECT * FROM apps")
|
|
||||||
.fetch_all(db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(a) => a,
|
|
||||||
Err(e) => { tracing::error!("restore_app_containers: DB error: {}", e); return; }
|
|
||||||
};
|
|
||||||
|
|
||||||
for app in &apps {
|
|
||||||
// Only care about apps that have at least one successful deploy.
|
|
||||||
let has_deploy: bool = sqlx::query_scalar(
|
|
||||||
"SELECT COUNT(*) > 0 FROM deploys WHERE app_id = ? AND status = 'success'"
|
|
||||||
)
|
|
||||||
.bind(&app.id)
|
|
||||||
.fetch_one(db)
|
|
||||||
.await
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if !has_deploy {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = format!("hiy-{}", app.id);
|
|
||||||
|
|
||||||
// Check container state via `podman inspect`.
|
|
||||||
let inspect = tokio::process::Command::new("podman")
|
|
||||||
.args(["inspect", "--format", "{{.State.Status}}", &container])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match inspect {
|
|
||||||
Ok(out) if out.status.success() => {
|
|
||||||
let status = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
||||||
if status == "running" {
|
|
||||||
tracing::debug!("restore_app_containers: {} already running", container);
|
|
||||||
} else {
|
|
||||||
tracing::info!("restore_app_containers: starting {} (was {})", container, status);
|
|
||||||
let start = tokio::process::Command::new("podman")
|
|
||||||
.args(["start", &container])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
match start {
|
|
||||||
Ok(o) if o.status.success() =>
|
|
||||||
tracing::info!("restore_app_containers: {} started", container),
|
|
||||||
Ok(o) =>
|
|
||||||
tracing::warn!("restore_app_containers: failed to start {}: {}",
|
|
||||||
container, String::from_utf8_lossy(&o.stderr).trim()),
|
|
||||||
Err(e) =>
|
|
||||||
tracing::warn!("restore_app_containers: error starting {}: {}", container, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
tracing::warn!(
|
|
||||||
"restore_app_containers: container {} not found — redeploy needed",
|
|
||||||
container
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::info!("restore_app_containers: done");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a visibility change to Caddy without requiring a full redeploy.
|
|
||||||
/// Best-effort: logs a warning on failure but does not surface an error to the caller.
|
|
||||||
async fn push_visibility_to_caddy(app_id: &str, port: i64, is_public: bool) {
|
|
||||||
if let Err(e) = try_push_visibility_to_caddy(app_id, port, is_public).await {
|
|
||||||
tracing::warn!("caddy visibility update for {}: {}", app_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_push_visibility_to_caddy(app_id: &str, port: i64, is_public: bool) -> anyhow::Result<()> {
|
|
||||||
let caddy_api = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://caddy:2019".into());
|
|
||||||
let domain = std::env::var("DOMAIN_SUFFIX").unwrap_or_else(|_| "localhost".into());
|
|
||||||
let app_host = format!("{}.{}", app_id, domain);
|
|
||||||
let upstream = format!("hiy-{}:{}", app_id, port);
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
// Discover the Caddy server name (Caddyfile adapter names it "srv0").
|
|
||||||
let servers: serde_json::Value = client
|
|
||||||
.get(format!("{}/config/apps/http/servers/", caddy_api))
|
|
||||||
.send().await?
|
|
||||||
.json().await?;
|
|
||||||
let server_name = servers.as_object()
|
|
||||||
.and_then(|m| m.keys().next().cloned())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no servers in Caddy config"))?;
|
|
||||||
|
|
||||||
let routes_url = format!("{}/config/apps/http/servers/{}/routes", caddy_api, server_name);
|
|
||||||
|
|
||||||
let routes: Vec<serde_json::Value> = client.get(&routes_url).send().await?.json().await?;
|
|
||||||
|
|
||||||
let dashboard = serde_json::json!({
|
|
||||||
"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "server:3000"}]}]
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut updated: Vec<serde_json::Value> = routes.into_iter()
|
|
||||||
.filter(|r| {
|
|
||||||
let is_this_app = r.pointer("/match/0/host")
|
|
||||||
.and_then(|h| h.as_array())
|
|
||||||
.map(|hosts| hosts.iter().any(|h| h.as_str() == Some(app_host.as_str())))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let is_catchall = r.get("match").is_none();
|
|
||||||
!is_this_app && !is_catchall
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
updated.insert(0, caddy_route(&app_host, &upstream, is_public));
|
|
||||||
updated.push(dashboard);
|
|
||||||
|
|
||||||
client.patch(&routes_url).json(&updated).send().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list(State(s): State<AppState>) -> Result<Json<Vec<App>>, StatusCode> {
|
pub async fn list(State(s): State<AppState>) -> Result<Json<Vec<App>>, StatusCode> {
|
||||||
let apps = sqlx::query_as::<_, App>("SELECT * FROM apps ORDER BY created_at DESC")
|
let apps = sqlx::query_as::<_, App>("SELECT * FROM apps ORDER BY created_at DESC")
|
||||||
.fetch_all(&s.db)
|
.fetch_all(&s.db)
|
||||||
|
|
@ -210,18 +29,10 @@ pub async fn create(
|
||||||
let now = Utc::now().to_rfc3339();
|
let now = Utc::now().to_rfc3339();
|
||||||
let branch = payload.branch.unwrap_or_else(|| "main".into());
|
let branch = payload.branch.unwrap_or_else(|| "main".into());
|
||||||
let secret = Uuid::new_v4().to_string().replace('-', "");
|
let secret = Uuid::new_v4().to_string().replace('-', "");
|
||||||
let memory_limit = payload.memory_limit.unwrap_or_else(|| "512m".into());
|
|
||||||
let cpu_limit = payload.cpu_limit.unwrap_or_else(|| "0.5".into());
|
|
||||||
let git_token_enc = payload.git_token
|
|
||||||
.as_deref()
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.map(crate::crypto::encrypt)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO apps (id, name, repo_url, branch, port, webhook_secret, memory_limit, cpu_limit, git_token, created_at, updated_at)
|
"INSERT INTO apps (id, name, repo_url, branch, port, webhook_secret, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&id)
|
.bind(&id)
|
||||||
.bind(&payload.name)
|
.bind(&payload.name)
|
||||||
|
|
@ -229,9 +40,6 @@ pub async fn create(
|
||||||
.bind(&branch)
|
.bind(&branch)
|
||||||
.bind(payload.port)
|
.bind(payload.port)
|
||||||
.bind(&secret)
|
.bind(&secret)
|
||||||
.bind(&memory_limit)
|
|
||||||
.bind(&cpu_limit)
|
|
||||||
.bind(&git_token_enc)
|
|
||||||
.bind(&now)
|
.bind(&now)
|
||||||
.bind(&now)
|
.bind(&now)
|
||||||
.execute(&s.db)
|
.execute(&s.db)
|
||||||
|
|
@ -281,44 +89,6 @@ pub async fn update(
|
||||||
.execute(&s.db).await
|
.execute(&s.db).await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
}
|
}
|
||||||
if let Some(v) = payload.memory_limit {
|
|
||||||
sqlx::query("UPDATE apps SET memory_limit = ?, updated_at = ? WHERE id = ?")
|
|
||||||
.bind(v).bind(&now).bind(&id)
|
|
||||||
.execute(&s.db).await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
}
|
|
||||||
if let Some(v) = payload.cpu_limit {
|
|
||||||
sqlx::query("UPDATE apps SET cpu_limit = ?, updated_at = ? WHERE id = ?")
|
|
||||||
.bind(v).bind(&now).bind(&id)
|
|
||||||
.execute(&s.db).await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
}
|
|
||||||
if let Some(v) = payload.is_public {
|
|
||||||
let flag: i64 = if v { 1 } else { 0 };
|
|
||||||
sqlx::query("UPDATE apps SET is_public = ?, updated_at = ? WHERE id = ?")
|
|
||||||
.bind(flag).bind(&now).bind(&id)
|
|
||||||
.execute(&s.db).await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
// Immediately reconfigure the Caddy route so the change takes effect
|
|
||||||
// without a full redeploy.
|
|
||||||
let app = fetch_app(&s, &id).await?;
|
|
||||||
push_visibility_to_caddy(&id, app.port, v).await;
|
|
||||||
}
|
|
||||||
if let Some(v) = payload.git_token {
|
|
||||||
if v.is_empty() {
|
|
||||||
sqlx::query("UPDATE apps SET git_token = NULL, updated_at = ? WHERE id = ?")
|
|
||||||
.bind(&now).bind(&id)
|
|
||||||
.execute(&s.db).await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
} else {
|
|
||||||
let enc = crate::crypto::encrypt(&v)
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
sqlx::query("UPDATE apps SET git_token = ?, updated_at = ? WHERE id = ?")
|
|
||||||
.bind(enc).bind(&now).bind(&id)
|
|
||||||
.execute(&s.db).await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch_app(&s, &id).await.map(Json)
|
fetch_app(&s, &id).await.map(Json)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{crypto, models::Database, AppState};
|
use crate::{models::Database, AppState};
|
||||||
|
|
||||||
type ApiError = (StatusCode, String);
|
type ApiError = (StatusCode, String);
|
||||||
|
|
||||||
|
|
@ -39,17 +39,13 @@ pub async fn get_db(
|
||||||
|
|
||||||
match db {
|
match db {
|
||||||
None => Err(err(StatusCode::NOT_FOUND, "No database provisioned")),
|
None => Err(err(StatusCode::NOT_FOUND, "No database provisioned")),
|
||||||
Some(d) => {
|
Some(d) => Ok(Json(json!({
|
||||||
let pw = crypto::decrypt(&d.pg_password)
|
"app_id": d.app_id,
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
"schema": d.app_id,
|
||||||
Ok(Json(json!({
|
"pg_user": d.pg_user,
|
||||||
"app_id": d.app_id,
|
"conn_str": conn_str(&d.pg_user, &d.pg_password),
|
||||||
"schema": d.app_id,
|
"created_at": d.created_at,
|
||||||
"pg_user": d.pg_user,
|
}))),
|
||||||
"conn_str": conn_str(&d.pg_user, &pw),
|
|
||||||
"created_at": d.created_at,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,31 +107,27 @@ pub async fn provision(
|
||||||
.execute(pg).await
|
.execute(pg).await
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Persist credentials (password encrypted at rest).
|
// Persist credentials.
|
||||||
let enc_password = crypto::encrypt(&password)
|
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO databases (app_id, pg_user, pg_password, created_at) VALUES (?, ?, ?, ?)",
|
"INSERT INTO databases (app_id, pg_user, pg_password, created_at) VALUES (?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&app_id)
|
.bind(&app_id)
|
||||||
.bind(&pg_user)
|
.bind(&pg_user)
|
||||||
.bind(&enc_password)
|
.bind(&password)
|
||||||
.bind(&now)
|
.bind(&now)
|
||||||
.execute(&s.db)
|
.execute(&s.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Inject DATABASE_URL as an encrypted app env var (picked up on next deploy).
|
// Inject DATABASE_URL as an app env var (picked up on next deploy).
|
||||||
let url = conn_str(&pg_user, &password);
|
let url = conn_str(&pg_user, &password);
|
||||||
let enc_url = crypto::encrypt(&url)
|
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO env_vars (app_id, key, value) VALUES (?, 'DATABASE_URL', ?)
|
"INSERT INTO env_vars (app_id, key, value) VALUES (?, 'DATABASE_URL', ?)
|
||||||
ON CONFLICT (app_id, key) DO UPDATE SET value = excluded.value",
|
ON CONFLICT (app_id, key) DO UPDATE SET value = excluded.value",
|
||||||
)
|
)
|
||||||
.bind(&app_id)
|
.bind(&app_id)
|
||||||
.bind(&enc_url)
|
.bind(&url)
|
||||||
.execute(&s.db)
|
.execute(&s.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use axum::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto,
|
|
||||||
models::{EnvVar, SetEnvVar},
|
models::{EnvVar, SetEnvVar},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
@ -21,12 +20,7 @@ pub async fn list(
|
||||||
.fetch_all(&s.db)
|
.fetch_all(&s.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
// Return keys only; values are masked in the UI and never sent in plaintext.
|
Ok(Json(vars))
|
||||||
let masked: Vec<EnvVar> = vars
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| EnvVar { value: "••••••••".into(), ..e })
|
|
||||||
.collect();
|
|
||||||
Ok(Json(masked))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set(
|
pub async fn set(
|
||||||
|
|
@ -34,15 +28,13 @@ pub async fn set(
|
||||||
Path(app_id): Path<String>,
|
Path(app_id): Path<String>,
|
||||||
Json(payload): Json<SetEnvVar>,
|
Json(payload): Json<SetEnvVar>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
let encrypted = crypto::encrypt(&payload.value)
|
|
||||||
.map_err(|e| { tracing::error!("encrypt env var: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?;
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO env_vars (app_id, key, value) VALUES (?, ?, ?)
|
"INSERT INTO env_vars (app_id, key, value) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(app_id, key) DO UPDATE SET value = excluded.value",
|
ON CONFLICT(app_id, key) DO UPDATE SET value = excluded.value",
|
||||||
)
|
)
|
||||||
.bind(&app_id)
|
.bind(&app_id)
|
||||||
.bind(&payload.key)
|
.bind(&payload.key)
|
||||||
.bind(&encrypted)
|
.bind(&payload.value)
|
||||||
.execute(&s.db)
|
.execute(&s.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
|
||||||
|
|
@ -278,8 +278,7 @@ pub async fn app_detail(
|
||||||
<button class="primary" onclick="provisionDb()">Provision Database</button></div>"#
|
<button class="primary" onclick="provisionDb()">Provision Database</button></div>"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
(true, Some(db)) => {
|
(true, Some(db)) => {
|
||||||
let pw = crate::crypto::decrypt(&db.pg_password).unwrap_or_default();
|
let url = format!("postgres://{}:{}@postgres:5432/hiy", db.pg_user, db.pg_password);
|
||||||
let url = format!("postgres://{}:{}@postgres:5432/hiy", db.pg_user, pw);
|
|
||||||
format!(r#"<div class="card"><h2>Database</h2>
|
format!(r#"<div class="card"><h2>Database</h2>
|
||||||
<table style="margin-bottom:16px">
|
<table style="margin-bottom:16px">
|
||||||
<tr><td style="width:160px">Schema</td><td><code>{schema}</code></td></tr>
|
<tr><td style="width:160px">Schema</td><td><code>{schema}</code></td></tr>
|
||||||
|
|
@ -297,45 +296,18 @@ pub async fn app_detail(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_public = app.is_public != 0;
|
|
||||||
let visibility_badge = if is_public {
|
|
||||||
r#"<span class="badge badge-success">public</span>"#
|
|
||||||
} else {
|
|
||||||
r#"<span class="badge badge-unknown">private</span>"#
|
|
||||||
};
|
|
||||||
let visibility_toggle_label = if is_public { "Make private" } else { "Make public" };
|
|
||||||
|
|
||||||
let (git_token_status, git_token_clear_btn) = if app.git_token.is_some() {
|
|
||||||
(
|
|
||||||
r#"<span class="badge badge-success">Token configured</span>"#.to_string(),
|
|
||||||
r#"<button class="danger" onclick="clearGitToken()">Clear</button>"#.to_string(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
r#"<span class="badge badge-unknown">No token — public repos only</span>"#.to_string(),
|
|
||||||
String::new(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = APP_DETAIL_TMPL
|
let body = APP_DETAIL_TMPL
|
||||||
.replace("{{name}}", &app.name)
|
.replace("{{name}}", &app.name)
|
||||||
.replace("{{repo}}", &app.repo_url)
|
.replace("{{repo}}", &app.repo_url)
|
||||||
.replace("{{branch}}", &app.branch)
|
.replace("{{branch}}", &app.branch)
|
||||||
.replace("{{port}}", &app.port.to_string())
|
.replace("{{port}}", &app.port.to_string())
|
||||||
.replace("{{host}}", &host)
|
.replace("{{host}}", &host)
|
||||||
.replace("{{app_id}}", &app.id)
|
.replace("{{app_id}}", &app.id)
|
||||||
.replace("{{secret}}", &app.webhook_secret)
|
.replace("{{secret}}", &app.webhook_secret)
|
||||||
.replace("{{memory_limit}}", &app.memory_limit)
|
.replace("{{deploy_rows}}", &deploy_rows)
|
||||||
.replace("{{cpu_limit}}", &app.cpu_limit)
|
.replace("{{env_rows}}", &env_rows)
|
||||||
.replace("{{deploy_rows}}", &deploy_rows)
|
.replace("{{c_badge}}", &container_badge(&container_state))
|
||||||
.replace("{{env_rows}}", &env_rows)
|
.replace("{{db_card}}", &db_card);
|
||||||
.replace("{{c_badge}}", &container_badge(&container_state))
|
|
||||||
.replace("{{db_card}}", &db_card)
|
|
||||||
.replace("{{git_token_status}}", &git_token_status)
|
|
||||||
.replace("{{git_token_clear_btn}}", &git_token_clear_btn)
|
|
||||||
.replace("{{visibility_badge}}", visibility_badge)
|
|
||||||
.replace("{{visibility_toggle_label}}", visibility_toggle_label)
|
|
||||||
.replace("{{is_public_js}}", if is_public { "true" } else { "false" });
|
|
||||||
|
|
||||||
Html(page(&app.name, &body)).into_response()
|
Html(page(&app.name, &body)).into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
· branch <code>{{branch}}</code>
|
· branch <code>{{branch}}</code>
|
||||||
· port <code>{{port}}</code>
|
· port <code>{{port}}</code>
|
||||||
· <a href="http://{{name}}.{{host}}" target="_blank">{{name}}.{{host}}</a>
|
· <a href="http://{{name}}.{{host}}" target="_blank">{{name}}.{{host}}</a>
|
||||||
· {{visibility_badge}}
|
|
||||||
<button style="font-size:0.78rem;padding:2px 10px;margin-left:4px" onclick="toggleVisibility()">{{visibility_toggle_label}}</button>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -35,42 +33,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<div class="row" style="margin-bottom:12px">
|
|
||||||
<div style="flex:3"><label>Repo URL</label><input id="cfg-repo" type="text" value="{{repo}}"></div>
|
|
||||||
<div style="flex:1"><label>Branch</label><input id="cfg-branch" type="text" value="{{branch}}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row" style="margin-bottom:12px">
|
|
||||||
<div style="flex:1"><label>Port</label><input id="cfg-port" type="number" value="{{port}}"></div>
|
|
||||||
<div style="flex:1"><label>Memory limit</label><input id="cfg-memory" type="text" value="{{memory_limit}}"></div>
|
|
||||||
<div style="flex:1"><label>CPU limit</label><input id="cfg-cpu" type="text" value="{{cpu_limit}}"></div>
|
|
||||||
</div>
|
|
||||||
<button class="primary" onclick="saveSettings()">Save</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{db_card}}
|
{{db_card}}
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Git Authentication</h2>
|
|
||||||
<p class="muted" style="margin-bottom:12px;font-size:0.9rem">
|
|
||||||
Required for private repos. Store a Personal Access Token (GitHub: <em>repo</em> scope,
|
|
||||||
GitLab: <em>read_repository</em>) so deploys can clone without interactive prompts.
|
|
||||||
Only HTTPS repo URLs are supported; SSH URLs use the server's own key pair.
|
|
||||||
</p>
|
|
||||||
<p style="margin-bottom:12px">{{git_token_status}}</p>
|
|
||||||
<div class="row">
|
|
||||||
<div style="flex:1">
|
|
||||||
<label>Personal Access Token</label>
|
|
||||||
<input id="git-token-input" type="password" placeholder="ghp_…">
|
|
||||||
</div>
|
|
||||||
<div style="align-self:flex-end;display:flex;gap:8px">
|
|
||||||
<button class="primary" onclick="saveGitToken()">Save</button>
|
|
||||||
{{git_token_clear_btn}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Environment Variables</h2>
|
<h2>Environment Variables</h2>
|
||||||
<div class="row" style="margin-bottom:16px">
|
<div class="row" style="margin-bottom:16px">
|
||||||
|
|
@ -181,53 +145,6 @@ async function deprovisionDb() {
|
||||||
if (r.ok) window.location.reload();
|
if (r.ok) window.location.reload();
|
||||||
else alert('Error: ' + await r.text());
|
else alert('Error: ' + await r.text());
|
||||||
}
|
}
|
||||||
const IS_PUBLIC = {{is_public_js}};
|
|
||||||
async function saveSettings() {
|
|
||||||
const body = {
|
|
||||||
repo_url: document.getElementById('cfg-repo').value,
|
|
||||||
branch: document.getElementById('cfg-branch').value,
|
|
||||||
port: parseInt(document.getElementById('cfg-port').value, 10),
|
|
||||||
memory_limit: document.getElementById('cfg-memory').value,
|
|
||||||
cpu_limit: document.getElementById('cfg-cpu').value,
|
|
||||||
};
|
|
||||||
const r = await fetch('/api/apps/' + APP_ID, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (r.ok) window.location.reload();
|
|
||||||
else alert('Error saving settings: ' + await r.text());
|
|
||||||
}
|
|
||||||
async function toggleVisibility() {
|
|
||||||
const r = await fetch('/api/apps/' + APP_ID, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({is_public: !IS_PUBLIC}),
|
|
||||||
});
|
|
||||||
if (r.ok) window.location.reload();
|
|
||||||
else alert('Error updating visibility: ' + await r.text());
|
|
||||||
}
|
|
||||||
async function saveGitToken() {
|
|
||||||
const tok = document.getElementById('git-token-input').value;
|
|
||||||
if (!tok) { alert('Enter a token first'); return; }
|
|
||||||
const r = await fetch('/api/apps/' + APP_ID, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({git_token: tok}),
|
|
||||||
});
|
|
||||||
if (r.ok) window.location.reload();
|
|
||||||
else alert('Error saving token: ' + await r.text());
|
|
||||||
}
|
|
||||||
async function clearGitToken() {
|
|
||||||
if (!confirm('Remove the stored git token for ' + APP_ID + '?')) return;
|
|
||||||
const r = await fetch('/api/apps/' + APP_ID, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({git_token: ''}),
|
|
||||||
});
|
|
||||||
if (r.ok) window.location.reload();
|
|
||||||
else alert('Error clearing token: ' + await r.text());
|
|
||||||
}
|
|
||||||
async function stopApp() {
|
async function stopApp() {
|
||||||
if (!confirm('Stop ' + APP_ID + '?')) return;
|
if (!confirm('Stop ' + APP_ID + '?')) return;
|
||||||
const r = await fetch('/api/apps/' + APP_ID + '/stop', {method:'POST'});
|
const r = await fetch('/api/apps/' + APP_ID + '/stop', {method:'POST'});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue