Compare commits

..

55 Commits

Author SHA1 Message Date
mbecker20
1374c26cd8 0.2.0 cleanup 2023-02-23 07:28:11 +00:00
mbecker20
5467b40b2e fix to git clone <TOKEN> splice 2023-02-23 07:22:32 +00:00
mbecker20
165b9012da improve users responsiveness 2023-02-23 07:17:18 +00:00
mbecker20
22630f665e update the manage users page 2023-02-23 07:09:33 +00:00
mbecker20
3d867084ba log poll default to false 2023-02-23 06:44:55 +00:00
mbecker20
171dd2d9e0 remove menu animation, change builder type to selector 2023-02-23 06:38:36 +00:00
mbecker20
9709239f88 build version h2 2023-02-22 22:45:05 +00:00
mbecker20
60d457b285 improve deployment in tree and display deployed version in header 2023-02-22 22:27:54 +00:00
mbecker20
8b1d4793a7 0.1.17 support building with ec2 instances 2023-02-22 21:05:03 +00:00
mbecker20
f2166c8435 configure aws config on builds 2023-02-22 20:49:56 +00:00
mbecker20
07d723a748 more prog on frontend, some api etc 2023-02-22 06:39:32 +00:00
mbecker20
b36f485287 put server / aws build on build header 2023-02-21 23:05:49 +00:00
mbecker20
a121ae0828 begin frontend refactor for ephemeral build support 2023-02-21 18:11:43 +00:00
mbecker20
e2b5a02008 building works 2023-02-21 05:22:26 +00:00
mbecker20
575aa62625 update versions to 0.1.16 2023-02-21 04:32:41 +00:00
mbecker20
ac88a2c4ed testing and fixes for aws build 2023-02-21 04:27:30 +00:00
mbecker20
f1dcb71a8a poll periphery on build instance to ensure connectivity before moving on 2023-02-20 22:56:22 +00:00
mbecker20
30d04bc201 support building on epheral ec2 2023-02-20 09:41:15 +00:00
mbecker20
33a00bb1a2 poll when instance running 2023-02-20 04:55:44 +00:00
mbecker20
ccca44ea89 start working on build instance spawn on aws 2023-02-20 01:19:07 +00:00
mbecker20
ae5f36fe51 0.1.15 use passkey for addition core - periphery auth layer 2023-02-17 18:29:13 +00:00
beckerinj
69ce1e4f36 start adding passkey auth to core periphery communication 2023-02-17 12:53:09 -05:00
mbecker20
6e444b9032 temp remove deployment state polling 2023-02-14 18:33:32 +00:00
beckerinj
73eff72da4 work on docs 2023-02-13 11:16:27 -05:00
beckerinj
698e3c214b opinionated 2023-02-12 14:48:28 -05:00
beckerinj
9da77667dc add next page 2023-02-12 04:14:06 -05:00
beckerinj
c30793fb8f to 2023-02-12 04:12:49 -05:00
beckerinj
84fdaab24d close intro better 2023-02-12 04:10:10 -05:00
beckerinj
cbd67bb609 back to table 2023-02-12 04:05:35 -05:00
beckerinj
00f58e9008 architecture 2023-02-12 03:59:26 -05:00
beckerinj
7738fab351 intro 2023-02-12 03:57:55 -05:00
beckerinj
06e8f6589b links 2023-02-12 00:55:14 -05:00
beckerinj
57d9287724 start docs 2023-02-12 00:51:05 -05:00
mbecker20
2cc65595ee log only polls when container is running 2023-02-11 20:29:25 +00:00
mbecker20
3dd2b97873 add polling for deployment state and logs 2023-02-11 20:17:13 +00:00
mbecker20
3c805ebbf7 fix deployment action build A tag styling 2023-02-11 19:36:36 +00:00
beckerinj
a854160018 Update periphery.config.example.toml 2023-02-08 13:40:00 -05:00
mbecker20
a99d9e5969 make update menu wider 2023-02-08 05:56:09 +00:00
mbecker20
813b6c1182 confirm menu 2023-02-08 05:37:27 +00:00
mbecker20
2958f9589b deployment / build delete wont fail if server disabled / unreachable. 2023-02-08 05:37:12 +00:00
beckerinj
69b4e26176 ConfirmMenuButton 2023-02-07 23:49:27 -05:00
beckerinj
78b00f139d delete server side effects 2023-02-07 23:23:46 -05:00
beckerinj
dc1e8de851 widen env 2023-02-07 23:23:18 -05:00
mbecker20
3187b335a3 cli 0.1.20 2023-02-07 20:12:20 +00:00
beckerinj
54b5a2b420 add logrotate to mongo startup 2023-02-07 15:10:36 -05:00
beckerinj
14c6bd00a8 use the scroller class for LogContainer 2023-02-02 16:14:11 -05:00
mbecker20
e9c3646450 0.1.14 fix deployment builder defaults 2023-01-30 17:45:34 +00:00
mbecker20
4f20257479 0.1.13 improve Builder structs with defaults 2023-01-30 17:24:03 +00:00
beckerinj
65749991de 0.1.12 MonitorClient::new_from_env 2023-01-30 11:50:48 -05:00
mbecker20
237a1d802d handle stuff when server disabled / unreachable 2023-01-28 09:03:00 +00:00
mbecker20
e4336f19f3 default copy server to curr server 2023-01-25 23:08:31 +00:00
mbecker20
c895e5e67f fix selector search behavior when using itemMap 2023-01-25 23:01:11 +00:00
mbecker20
4e4e210736 implement build / deployment copy 2023-01-25 22:53:27 +00:00
beckerinj
09dfc8faa3 cli 0.1.19 add Restart=on-failure to service unit file 2023-01-25 14:58:54 -05:00
mbecker20
3c4f77cc78 0.1.11 custom DockerRunArgs Default implementation 2023-01-25 06:29:09 +00:00
117 changed files with 3463 additions and 1118 deletions

17
.vscode/tasks.json vendored
View File

@@ -92,15 +92,6 @@
"cwd": "${workspaceFolder}/lib/types"
}
},
{
"type": "cargo",
"command": "publish",
"args": ["--allow-dirty"],
"label": "publish monitor helpers",
"options": {
"cwd": "${workspaceFolder}/lib/helpers"
}
},
{
"type": "cargo",
"command": "publish",
@@ -109,14 +100,6 @@
"cwd": "${workspaceFolder}/lib/monitor_client"
}
},
{
"type": "cargo",
"command": "publish",
"label": "publish monitor periphery",
"options": {
"cwd": "${workspaceFolder}/periphery"
}
},
{
"type": "cargo",
"command": "publish",

641
Cargo.lock generated
View File

@@ -19,6 +19,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -75,6 +84,322 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3d1e2a1f1ab3ac6c4b884e37413eaa03eb9d901e4fc68ee8f5c1d49721680e"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-sdk-sso",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"hex",
"http",
"hyper",
"ring",
"time 0.3.17",
"tokio",
"tower",
"tracing",
"zeroize",
]
[[package]]
name = "aws-credential-types"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0696a0523a39a19087747e4dafda0362dc867531e3d72a3f195564c84e5e08"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-endpoint"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80a4f935ab6a1919fbfd6102a80c4fccd9ff5f47f94ba154074afe1051903261"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-http"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82976ca4e426ee9ca3ffcf919d9b2c8d14d0cd80d43cc02173737a8f07f28d4d"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http-body",
"lazy_static",
"percent-encoding",
"pin-project-lite",
"tracing",
]
[[package]]
name = "aws-sdk-ec2"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b40ee2d853d8300a49513778beb79b1574ff9e9c94b30b1531bc0171d730ad64"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-query",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"fastrand",
"http",
"regex",
"tokio-stream",
"tower",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0119bacf0c42f587506769390983223ba834e605f049babe514b2bd646dbb2"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"regex",
"tokio-stream",
"tower",
]
[[package]]
name = "aws-sdk-sts"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "270b6a33969ebfcb193512fbd5e8ee5306888ad6c6d5d775cdbfb2d50d94de26"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-query",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"http",
"regex",
"tower",
"tracing",
]
[[package]]
name = "aws-sig-auth"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "660a02a98ab1af83bd8d714afbab2d502ba9b18c49e7e4cddd6bf8837ff778cb"
dependencies = [
"aws-credential-types",
"aws-sigv4",
"aws-smithy-http",
"aws-types",
"http",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdaf11005b7444e6cd66f600d09861a3aeb6eb89a0f003c7c9820dbab2d15297"
dependencies = [
"aws-smithy-http",
"form_urlencoded",
"hex",
"hmac",
"http",
"once_cell",
"percent-encoding",
"regex",
"sha2",
"time 0.3.17",
"tracing",
]
[[package]]
name = "aws-smithy-async"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e8615bf58d144dec3fcdb5110941b84e904c68054cb74ed240b9588fc337a5"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
"tokio-stream",
]
[[package]]
name = "aws-smithy-client"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c1df4c1d03e1ce299ae4e24c19d0f4cd8bebceac60828530e579977d70289a"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-types",
"bytes",
"fastrand",
"http",
"http-body",
"hyper",
"hyper-rustls",
"lazy_static",
"pin-project-lite",
"tokio",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-http"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78abf16f8667b9176737cfffd1dd4ad07d350ef5dba01d01fdec5f31265f7134"
dependencies = [
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"hyper",
"once_cell",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "aws-smithy-http-tower"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d517ac2476efc1820228c2fdfdcb17d3bea8695558bd67584a62a47c12b41918"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a23cc091168c5d969b150d7cc11a5a202bfa88dc36494e6659d2499b0cf227b"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92a1218021362bc1faa56648397b8cc4ac7631a3944e087d314d0187ef88d782"
dependencies = [
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-types"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8d2056dc5f10094d5e753ac5c649e8996869f0649b641e470950151596db73"
dependencies = [
"base64-simd",
"itoa",
"num-integer",
"ryu",
"time 0.3.17",
]
[[package]]
name = "aws-smithy-xml"
version = "0.54.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a3029cbb4a49656456b3f2b34daee7f68dd93c61cc5d03fa90788cb1d25d5b4"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f15b34253b68cde08e39b0627cc6101bcca64351229484b4743392c035d057"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-types",
"http",
"rustc_version 0.4.0",
"tracing",
]
[[package]]
name = "axum"
version = "0.6.4"
@@ -130,9 +455,9 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d"
checksum = "51227033e4d3acad15c879092ac8a228532707b5db5ff2628f638334f63e1b7a"
dependencies = [
"axum",
"bytes",
@@ -181,14 +506,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bcrypt"
version = "0.13.0"
name = "base64-simd"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641"
checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
dependencies = [
"base64 0.13.1",
"simd-abstraction",
]
[[package]]
name = "bcrypt"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a"
dependencies = [
"base64 0.21.0",
"blowfish",
"getrandom",
"subtle",
"zeroize",
]
@@ -299,10 +634,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
name = "cc"
version = "1.0.78"
name = "bytes-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
dependencies = [
"bytes",
"either",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
@@ -398,7 +743,7 @@ dependencies = [
[[package]]
name = "core"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -414,8 +759,8 @@ dependencies = [
"hex",
"hmac",
"jwt",
"monitor_helpers 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_helpers",
"monitor_types 0.2.0",
"mungos",
"periphery_client",
"serde",
@@ -520,9 +865,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e"
checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -532,9 +877,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200"
checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8"
dependencies = [
"cc",
"codespan-reporting",
@@ -547,15 +892,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea"
checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971"
[[package]]
name = "cxxbridge-macro"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e"
checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e"
dependencies = [
"proc-macro2",
"quote",
@@ -650,10 +995,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "db_client"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.0",
"mungos",
]
@@ -740,9 +1085,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "either"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "encoding_rs"
@@ -846,9 +1191,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [
"futures-channel",
"futures-core",
@@ -861,9 +1206,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
"futures-sink",
@@ -871,15 +1216,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
[[package]]
name = "futures-executor"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
dependencies = [
"futures-core",
"futures-task",
@@ -888,15 +1233,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
[[package]]
name = "futures-macro"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
dependencies = [
"proc-macro2",
"quote",
@@ -905,21 +1250,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
[[package]]
name = "futures-task"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
[[package]]
name = "futures-util"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
"futures-channel",
"futures-core",
@@ -1103,6 +1448,21 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
dependencies = [
"http",
"hyper",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -1453,7 +1813,7 @@ dependencies = [
"rand",
"rustc_version_runtime",
"rustls",
"rustls-pemfile",
"rustls-pemfile 0.3.0",
"serde",
"serde_bytes",
"serde_with",
@@ -1478,7 +1838,7 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "0.1.17"
version = "0.1.23"
dependencies = [
"async_timing_util",
"clap",
@@ -1489,16 +1849,17 @@ dependencies = [
"serde_derive",
"strum",
"strum_macros",
"toml 0.5.11",
"toml",
]
[[package]]
name = "monitor_client"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"envy",
"futures-util",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
@@ -1510,47 +1871,30 @@ dependencies = [
[[package]]
name = "monitor_helpers"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"async_timing_util",
"aws-config",
"aws-sdk-ec2",
"axum",
"bollard",
"futures",
"futures-util",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.0",
"periphery_client",
"rand",
"run_command",
"serde",
"serde_derive",
"serde_json",
"toml 0.6.0",
]
[[package]]
name = "monitor_helpers"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55fd1c4d2dc82a223f96c81ac825b6c0e6b955eedb3542e03d1a75a9df94a7"
dependencies = [
"anyhow",
"async_timing_util",
"axum",
"bollard",
"futures",
"futures-util",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"rand",
"run_command",
"serde",
"serde_derive",
"serde_json",
"toml 0.6.0",
"tokio",
"toml",
]
[[package]]
name = "monitor_periphery"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -1561,8 +1905,8 @@ dependencies = [
"dotenv",
"envy",
"futures-util",
"monitor_helpers 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_helpers",
"monitor_types 0.2.0",
"run_command",
"serde",
"serde_derive",
@@ -1570,13 +1914,13 @@ dependencies = [
"sysinfo",
"tokio",
"tokio-util",
"toml 0.5.11",
"toml",
"tower",
]
[[package]]
name = "monitor_types"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"bollard",
@@ -1593,9 +1937,9 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.1.10"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa4230da5cad59b690e3f97b24396b11e1e716cfa530ca79aebc8aae9d39e36"
checksum = "62e9379ee43474ebac60b639f9094343b92cc9cbee55d8a447a65cd46305feee"
dependencies = [
"anyhow",
"bollard",
@@ -1814,6 +2158,12 @@ version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "outref"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
[[package]]
name = "parking_lot"
version = "0.12.1"
@@ -1854,12 +2204,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "periphery_client"
version = "0.1.10"
version = "0.2.0"
dependencies = [
"anyhow",
"futures-util",
"monitor_helpers 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.0",
"reqwest",
"serde",
"serde_json",
@@ -2020,6 +2369,23 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@@ -2106,7 +2472,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
"semver 0.9.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.16",
]
[[package]]
@@ -2115,8 +2490,8 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f"
dependencies = [
"rustc_version",
"semver",
"rustc_version 0.2.3",
"semver 0.9.0",
]
[[package]]
@@ -2145,6 +2520,18 @@ dependencies = [
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile 1.0.2",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "0.3.0"
@@ -2154,6 +2541,15 @@ dependencies = [
"base64 0.13.1",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustversion"
version = "1.0.11"
@@ -2199,9 +2595,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721"
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
dependencies = [
"bitflags",
"core-foundation",
@@ -2229,6 +2625,12 @@ dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]]
name = "semver-parser"
version = "0.7.0"
@@ -2370,6 +2772,15 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-abstraction"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
dependencies = [
"outref",
]
[[package]]
name = "slab"
version = "0.4.7"
@@ -2482,9 +2893,9 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]]
name = "sysinfo"
version = "0.27.7"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975fe381e0ecba475d4acff52466906d95b153a40324956552e027b2a9eaa89e"
checksum = "727220a596b4ca0af040a07091e49f5c105ec8f2592674339a5bf35be592f76e"
dependencies = [
"cfg-if",
"core-foundation-sys",
@@ -2614,9 +3025,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.24.2"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [
"autocfg",
"bytes",
@@ -2664,6 +3075,17 @@ dependencies = [
"webpki",
]
[[package]]
name = "tokio-stream"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.18.0"
@@ -2694,18 +3116,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.5.11"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
checksum = "2f560bc7fb3eb31f5eee1340c68a2160cad39605b7b9c9ec32045ddbdee13b85"
dependencies = [
"serde",
"serde_spanned",
@@ -2715,18 +3128,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
checksum = "886f31a9b85b6182cabd4d8b07df3b451afcc216563748201490940d2a28ed36"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581"
checksum = "233d8716cdc5d20ec88a18a839edaf545edc71efa4a5ff700ef4a102c26cd8fa"
dependencies = [
"indexmap",
"nom8",
@@ -2802,9 +3215,21 @@ dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.30"
@@ -3240,6 +3665,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "xmlparser"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "zeroize"
version = "1.5.7"

View File

@@ -3,8 +3,8 @@ WORKDIR /builder
COPY ./core ./core
# COPY ./lib/types ./lib/types
# COPY ./lib/helpers ./lib/helpers
COPY ./lib/types ./lib/types
COPY ./lib/helpers ./lib/helpers
COPY ./lib/db_client ./lib/db_client
COPY ./lib/periphery_client ./lib/periphery_client

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_cli"
version = "0.1.17"
version = "0.1.23"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"
@@ -18,7 +18,7 @@ async_timing_util = "0.1.14"
rand = "0.8"
serde = "1.0"
serde_derive = "1.0"
toml = "0.5"
toml = "0.7"
run_command = "0.0.5"
colored = "2"
strum = "0.24"

View File

@@ -74,6 +74,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
local_auth: true,
github_oauth: Default::default(),
google_oauth: Default::default(),
aws: Default::default(),
mongo: MongoConfig {
uri: mongo_uri,
db_name: mongo_db_name,
@@ -81,6 +82,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
},
jwt_secret: generate_secret(40),
github_webhook_secret: generate_secret(30),
passkey: generate_secret(30),
};
write_to_toml(&path, &config);
@@ -176,7 +178,7 @@ pub fn start_mongo(sub_matches: &ArgMatches) {
}
}
let command = format!("docker stop {name} && docker container rm {name} && docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} mongo --quiet");
let command = format!("docker stop {name} && docker container rm {name} && docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} --log-opt max-size=15m --log-opt max-file=3 mongo --quiet");
let output = run_command_pipe_to_terminal(&command);
@@ -318,9 +320,10 @@ pub fn gen_periphery_config(sub_matches: &ArgMatches) {
let config = PeripheryConfig {
port,
repo_dir,
stats_polling_rate,
allowed_ips,
repo_dir,
passkeys: vec![],
secrets: Default::default(),
github_accounts: Default::default(),
docker_accounts: Default::default(),
@@ -588,6 +591,7 @@ Description=agent to connect with monitor core
[Service]
ExecStart={home}/.monitor/bin/periphery --config-path {config_path} --home-dir {home}
Restart=on-failure
TimeoutStartSec=0
[Install]

View File

@@ -21,7 +21,6 @@ pub struct CoreConfig {
#[serde(default)]
pub keep_stats_for_days: u64, // 0 means never prune
// jwt config
pub jwt_secret: String,
#[serde(default = "default_jwt_valid_for")]
pub jwt_valid_for: Timelength,
@@ -32,20 +31,25 @@ pub struct CoreConfig {
// used to verify validity from github webhooks
pub github_webhook_secret: String,
// sent in auth header with req to periphery
pub passkey: String,
// integration with slack app
pub slack_url: Option<String>,
// enable login with local auth
pub local_auth: bool,
// github integration
pub mongo: MongoConfig,
#[serde(default)]
pub github_oauth: OauthCredentials,
// google integration
#[serde(default)]
pub google_oauth: OauthCredentials,
// mongo config
pub mongo: MongoConfig,
#[serde(default)]
pub aws: AwsBuilderConfig,
}
fn default_core_port() -> u16 {
@@ -83,6 +87,60 @@ fn default_core_mongo_db_name() -> String {
"monitor".to_string()
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AwsBuilderConfig {
#[serde(skip_serializing)]
pub access_key_id: String,
#[serde(skip_serializing)]
pub secret_access_key: String,
pub default_ami_id: String,
pub default_subnet_id: String,
pub default_key_pair_name: String,
#[serde(default)]
pub available_ami_accounts: AvailableAmiAccounts,
#[serde(default = "default_aws_region")]
pub default_region: String,
#[serde(default = "default_volume_gb")]
pub default_volume_gb: i32,
#[serde(default = "default_instance_type")]
pub default_instance_type: String,
#[serde(default)]
pub default_security_group_ids: Vec<String>,
#[serde(default)]
pub default_assign_public_ip: bool,
}
fn default_aws_region() -> String {
String::from("us-east-1")
}
fn default_volume_gb() -> i32 {
8
}
fn default_instance_type() -> String {
String::from("m5.2xlarge")
}
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AmiAccounts {
pub name: String,
#[serde(default)]
pub github: Vec<String>,
#[serde(default)]
pub docker: Vec<String>,
}
pub type GithubUsername = String;
pub type GithubToken = String;
pub type GithubAccounts = HashMap<GithubUsername, GithubToken>;
@@ -104,6 +162,8 @@ pub struct PeripheryConfig {
#[serde(default)]
pub allowed_ips: Vec<IpAddr>,
#[serde(default)]
pub passkeys: Vec<String>,
#[serde(default)]
pub secrets: SecretsMap,
#[serde(default)]
pub github_accounts: GithubAccounts,

View File

@@ -22,17 +22,37 @@ slack_url = "your_slack_app_webhook_url"
# token that has to be given to github during webhook config as the Secret
github_webhook_secret = "your_random_webhook_secret"
# token used to authenticate core requests to periphery
passkey = "your_random_passkey"
# can be 30-sec, 1-min, 2-min, 5-min
monitoring_interval = "1-min"
# allow or deny user login with username / password
local_auth = true
[aws]
access_key_id = "your_aws_key_id"
secret_access_key = "your_aws_secret_key"
default_region = "us-east-1"
default_ami_id = "your_periphery_ami"
default_key_pair_name = "your_default_key_pair_name"
default_instance_type = "m5.2xlarge"
default_volume_gb = 8
default_subnet_id = "your_default_subnet_id"
default_security_group_ids = ["sg_id_1", "sg_id_2"]
default_assign_public_ip = false
[aws.available_ami_accounts]
your_periphery_ami = { name = "default ami", github = ["github_username"], docker = ["docker_username"] }
[github_oauth]
enabled = true
id = "your_github_client_id"
secret = "your_github_client_secret"
[google_oauth]
enabled = true
id = "your_google_client_id"
secret = "your_google_client_secret"

View File

@@ -1,7 +1,8 @@
port = 9001 # optional. 9001 is default
port = 8000 # optional. 8000 is default
repo_dir = "/repos" # optional. /repos is default. no reason to change if running the docker container, just mount your desired repo dir to /repos in the container
stats_polling_rate = "5-sec" # optional. 5-sec is default. can use 1-sec, 5-sec, 10-sec, 30-sec, 1-min. controls granularity of system stats recorded
allowed_ips = ["127.0.0.1"] # optional. default is empty, which will not block any request by ip.
passkeys = ["abcdefghijk"] # optional. default is empty, which will not require any passkey to be passed by core.
[secrets] # optional. can inject these values into your deployments configuration.
secret_variable = "secret_value"
@@ -12,4 +13,4 @@ github_username2 = "github_token2"
[docker_accounts] # optional
docker_username1 = "docker_token1"
docker_username2 = "docker_token2"
docker_username2 = "docker_token2"

View File

@@ -1,23 +1,21 @@
[package]
name = "core"
version = "0.1.10"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# helpers = { package = "monitor_helpers", path = "../lib/helpers" }
# types = { package = "monitor_types", path = "../lib/types" }
helpers = { package = "monitor_helpers", version = "0.1.10" }
types = { package = "monitor_types", version = "0.1.10" }
helpers = { package = "monitor_helpers", path = "../lib/helpers" }
types = { package = "monitor_types", path = "../lib/types" }
db = { package = "db_client", path = "../lib/db_client" }
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
axum_oauth2 = { path = "../lib/axum_oauth2" }
tokio = { version = "1.24", features = ["full"] }
tokio = { version = "1.25", features = ["full"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio-util = "0.7"
axum = { version = "0.6", features = ["ws", "json"] }
axum-extra = { version = "0.4", features = ["spa"] }
axum-extra = { version = "0.5", features = ["spa"] }
tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.3", features = ["cors"] }
slack = { package = "slack_client_rs", version = "0.0.8" }
@@ -28,7 +26,7 @@ serde_json = "1.0"
dotenv = "0.15"
envy = "0.4"
anyhow = "1.0"
bcrypt = "0.13"
bcrypt = "0.14"
jwt = "0.16"
hmac = "0.12"
sha2 = "0.10"

View File

@@ -1,18 +1,23 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use diff::Diff;
use helpers::{all_logs_success, to_monitor_name};
use helpers::{
all_logs_success,
aws::{self, create_ec2_client, create_instance_with_ami, terminate_ec2_instance, Ec2Instance},
to_monitor_name,
};
use mungos::{doc, to_bson};
use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget,
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version,
};
use crate::{
auth::RequestUser,
helpers::{any_option_diff_is_some, option_diff_is_some},
state::State,
};
use crate::{auth::RequestUser, state::State};
const BUILDER_POLL_RATE_SECS: u64 = 2;
const BUILDER_POLL_MAX_TRIES: usize = 30;
impl State {
pub async fn get_build_check_permissions(
@@ -39,18 +44,13 @@ impl State {
}
}
pub async fn create_build(
&self,
name: &str,
server_id: String,
user: &RequestUser,
) -> anyhow::Result<Build> {
self.get_server_check_permissions(&server_id, user, PermissionLevel::Update)
.await?;
pub async fn create_build(&self, name: &str, user: &RequestUser) -> anyhow::Result<Build> {
if !user.is_admin && !user.create_build_permissions {
return Err(anyhow!("user does not have permission to create builds"));
}
let start_ts = monitor_timestamp();
let build = Build {
name: to_monitor_name(name),
server_id,
permissions: [(user.id.clone(), PermissionLevel::Update)]
.into_iter()
.collect(),
@@ -84,10 +84,7 @@ impl State {
mut build: Build,
user: &RequestUser,
) -> anyhow::Result<Build> {
build.id = self
.create_build(&build.name, build.server_id.clone(), user)
.await?
.id;
build.id = self.create_build(&build.name, user).await?.id;
let build = self.update_build(build, user).await?;
Ok(build)
}
@@ -96,14 +93,13 @@ impl State {
&self,
target_id: &str,
new_name: String,
new_server_id: String,
user: &RequestUser,
) -> anyhow::Result<Build> {
let mut build = self
.get_build_check_permissions(target_id, user, PermissionLevel::Update)
.await?;
build.name = new_name;
build.server_id = new_server_id;
build.version = Version::default();
let build = self.create_full_build(build, user).await?;
Ok(build)
}
@@ -116,12 +112,6 @@ impl State {
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let start_ts = monitor_timestamp();
let server = self.db.get_server(&build.server_id).await?;
let delete_repo_log = self
.periphery
.delete_repo(&server, &build.name)
.await
.context("failed at deleting repo")?;
self.db.builds.delete_one(build_id).await?;
let update = Update {
target: UpdateTarget::Build(build_id.to_string()),
@@ -129,13 +119,10 @@ impl State {
start_ts,
end_ts: Some(monitor_timestamp()),
operator: user.id.clone(),
logs: vec![
delete_repo_log,
Log::simple(
"delete build",
format!("deleted build {} on server {}", build.name, server.name),
),
],
logs: vec![Log::simple(
"delete build",
format!("deleted build {}", build.name),
)],
success: true,
..Default::default()
};
@@ -171,16 +158,25 @@ impl State {
mut new_build: Build,
user: &RequestUser,
) -> anyhow::Result<Build> {
let start_ts = monitor_timestamp();
let current_build = self
.get_build_check_permissions(&new_build.id, user, PermissionLevel::Update)
.await?;
let start_ts = monitor_timestamp();
if let Some(new_server_id) = &new_build.server_id {
if current_build.server_id.is_none()
|| new_server_id != current_build.server_id.as_ref().unwrap()
{
self.get_server_check_permissions(new_server_id, user, PermissionLevel::Update)
.await
.context("user does not have permission to attach build to this server")?;
}
}
// none of these should be changed through this method
new_build.name = current_build.name.clone();
new_build.permissions = current_build.permissions.clone();
new_build.server_id = current_build.server_id.clone();
new_build.last_built_at = String::new();
new_build.last_built_at = current_build.last_built_at.clone();
new_build.created_at = current_build.created_at.clone();
new_build.updated_at = start_ts.clone();
@@ -192,41 +188,42 @@ impl State {
let diff = current_build.diff(&new_build);
let mut update = Update {
let update = Update {
operation: Operation::UpdateBuild,
target: UpdateTarget::Build(new_build.id.clone()),
start_ts,
status: UpdateStatus::InProgress,
status: UpdateStatus::Complete,
logs: vec![Log::simple(
"build update",
serde_json::to_string_pretty(&diff).unwrap(),
)],
operator: user.id.clone(),
end_ts: Some(monitor_timestamp()),
success: true,
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
// update.id = self.add_update(update.clone()).await?;
if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
|| option_diff_is_some(&diff.on_clone)
{
let server = self.db.get_server(&current_build.server_id).await?;
match self.periphery.clone_repo(&server, &new_build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
}
Err(e) => update
.logs
.push(Log::error("cloning repo", format!("{e:#?}"))),
}
}
// if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
// || option_diff_is_some(&diff.on_clone)
// {
// let server = self.db.get_server(&current_build.server_id).await?;
// match self.periphery.clone_repo(&server, &new_build).await {
// Ok(clone_logs) => {
// update.logs.extend(clone_logs);
// }
// Err(e) => update
// .logs
// .push(Log::error("cloning repo", format!("{e:#?}"))),
// }
// }
update.end_ts = Some(monitor_timestamp());
update.success = all_logs_success(&update.logs);
update.status = UpdateStatus::Complete;
// update.end_ts = Some(monitor_timestamp());
// update.success = all_logs_success(&update.logs);
// update.status = UpdateStatus::Complete;
self.update_update(update).await?;
self.add_update(update).await?;
Ok(new_build)
}
@@ -253,10 +250,7 @@ impl State {
let mut build = self
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let server = self.db.get_server(&build.server_id).await?;
build.version.increment();
let mut update = Update {
target: UpdateTarget::Build(build_id.to_string()),
operation: Operation::BuildBuild,
@@ -267,12 +261,95 @@ impl State {
version: build.version.clone().into(),
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
let (server, aws_client) = if let Some(server_id) = &build.server_id {
let server = self.db.get_server(server_id).await;
if let Err(e) = server {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update
.logs
.push(Log::error("get build server", format!("{e:#?}")));
self.update_update(update.clone()).await?;
return Err(e);
}
let server = Ec2Instance {
instance_id: String::new(),
server: server.unwrap(),
};
(server, None)
} else if build.aws_config.is_some() {
let start_ts = monitor_timestamp();
let res = self.create_ec2_instance_for_build(&build).await;
if let Err(e) = res {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update.logs.push(Log {
stage: "start build server".to_string(),
stderr: format!("{e:#?}"),
success: false,
start_ts,
end_ts: monitor_timestamp(),
..Default::default()
});
self.update_update(update).await?;
return Err(e);
}
let (server, aws_client, logs) = res.unwrap();
update.logs.extend(logs);
self.update_update(update.clone()).await?;
(server, aws_client)
} else {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update.logs.push(Log::error(
"start build",
"build has neither server_id nor aws_config attached".to_string(),
));
self.update_update(update).await?;
return Err(anyhow!(
"build has neither server_id or aws_config attached"
));
};
let clone_success = match self.periphery.clone_repo(&server.server, &build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
true
}
Err(e) => {
update
.logs
.push(Log::error("clone repo", format!("{e:#?}")));
false
}
};
if !clone_success {
let _ = self
.periphery
.delete_repo(&server.server, &build.name)
.await;
if let Some(aws_client) = aws_client {
self.terminate_ec2_instance(aws_client, &server, &mut update)
.await;
}
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
self.update_update(update.clone()).await?;
return Ok(update);
}
self.update_update(update.clone()).await?;
let build_logs = match self
.periphery
.build(&server, &build)
.build(&server.server, &build)
.await
.context("failed at call to periphery to build")
{
@@ -282,9 +359,9 @@ impl State {
match build_logs {
Some(logs) => {
let success = all_logs_success(&logs);
update.logs.extend(logs);
update.success = all_logs_success(&update.logs);
if update.success {
if success {
let _ = self
.db
.builds
@@ -305,73 +382,200 @@ impl State {
.push(Log::error("build", "builder busy".to_string()));
}
}
let _ = self
.periphery
.delete_repo(&server.server, &build.name)
.await;
if let Some(aws_client) = aws_client {
self.terminate_ec2_instance(aws_client, &server, &mut update)
.await;
}
update.success = all_logs_success(&update.logs);
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
self.update_update(update.clone()).await?;
Ok(update)
}
pub async fn reclone_build(
async fn create_ec2_instance_for_build(
&self,
build_id: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
if self.build_busy(build_id).await {
return Err(anyhow!("build busy"));
build: &Build,
) -> anyhow::Result<(Ec2Instance, Option<aws::Client>, Vec<Log>)> {
if build.aws_config.is_none() {
return Err(anyhow!("build has no aws_config attached"));
}
{
let mut lock = self.build_action_states.lock().await;
let entry = lock.entry(build_id.to_string()).or_default();
entry.recloning = true;
}
let res = self.reclone_build_inner(build_id, user).await;
{
let mut lock = self.build_action_states.lock().await;
let entry = lock.entry(build_id.to_string()).or_default();
entry.recloning = false;
}
res
}
async fn reclone_build_inner(
&self,
build_id: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
let build = self
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let server = self.db.get_server(&build.server_id).await?;
let mut update = Update {
target: UpdateTarget::Build(build_id.to_string()),
operation: Operation::RecloneBuild,
start_ts: monitor_timestamp(),
status: UpdateStatus::InProgress,
operator: user.id.clone(),
let start_instance_ts = monitor_timestamp();
let aws_config = build.aws_config.as_ref().unwrap();
let region = aws_config
.region
.as_ref()
.unwrap_or(&self.config.aws.default_region)
.to_string();
let aws_client = create_ec2_client(
region,
&self.config.aws.access_key_id,
self.config.aws.secret_access_key.clone(),
)
.await;
let ami_id = aws_config
.ami_id
.as_ref()
.unwrap_or(&self.config.aws.default_ami_id);
let instance_type = aws_config
.instance_type
.as_ref()
.unwrap_or(&self.config.aws.default_instance_type);
let subnet_id = aws_config
.subnet_id
.as_ref()
.unwrap_or(&self.config.aws.default_subnet_id);
let security_group_ids = aws_config
.security_group_ids
.as_ref()
.unwrap_or(&self.config.aws.default_security_group_ids)
.to_owned();
let readable_sec_group_ids = security_group_ids.join(", ");
let volume_size_gb = *aws_config
.volume_gb
.as_ref()
.unwrap_or(&self.config.aws.default_volume_gb);
let key_pair_name = aws_config
.key_pair_name
.as_ref()
.unwrap_or(&self.config.aws.default_key_pair_name);
let assign_public_ip = *aws_config
.assign_public_ip
.as_ref()
.unwrap_or(&self.config.aws.default_assign_public_ip);
let instance = create_instance_with_ami(
&aws_client,
&format!("BUILDER-{}-v{}", build.name, build.version.to_string()),
ami_id,
instance_type,
subnet_id,
security_group_ids,
volume_size_gb,
key_pair_name,
assign_public_ip,
)
.await?;
let instance_id = &instance.instance_id;
let start_log = Log {
stage: "start build instance".to_string(),
success: true,
stdout: format!("instance id: {instance_id}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_size_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}"),
start_ts: start_instance_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
update.success = match self.periphery.clone_repo(&server, &build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
true
let start_connect_ts = monitor_timestamp();
let mut res = Ok(String::new());
for _ in 0..BUILDER_POLL_MAX_TRIES {
let status = self.periphery.health_check(&instance.server).await;
if let Ok(_) = status {
let connect_log = Log {
stage: "build instance connected".to_string(),
success: true,
stdout: "established contact with periphery on builder".to_string(),
start_ts: start_connect_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
return Ok((instance, Some(aws_client), vec![start_log, connect_log]));
}
Err(e) => {
update
.logs
.push(Log::error("clone repo", format!("{e:#?}")));
false
}
};
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
self.update_update(update.clone()).await?;
Ok(update)
res = status;
tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS)).await;
}
let _ = terminate_ec2_instance(&aws_client, &instance.instance_id).await;
Err(anyhow!(
"unable to reach periphery agent on build server\n{res:#?}"
))
}
async fn terminate_ec2_instance(
&self,
aws_client: aws::Client,
server: &Ec2Instance,
update: &mut Update,
) {
let res = terminate_ec2_instance(&aws_client, &server.instance_id).await;
if let Err(e) = res {
update
.logs
.push(Log::error("terminate instance", format!("{e:#?}")))
} else {
update.logs.push(Log::simple(
"terminate instance",
format!("terminate instance id {}", server.instance_id),
))
}
}
// pub async fn reclone_build(
// &self,
// build_id: &str,
// user: &RequestUser,
// ) -> anyhow::Result<Update> {
// if self.build_busy(build_id).await {
// return Err(anyhow!("build busy"));
// }
// {
// let mut lock = self.build_action_states.lock().await;
// let entry = lock.entry(build_id.to_string()).or_default();
// entry.recloning = true;
// }
// let res = self.reclone_build_inner(build_id, user).await;
// {
// let mut lock = self.build_action_states.lock().await;
// let entry = lock.entry(build_id.to_string()).or_default();
// entry.recloning = false;
// }
// res
// }
// async fn reclone_build_inner(
// &self,
// build_id: &str,
// user: &RequestUser,
// ) -> anyhow::Result<Update> {
// let build = self
// .get_build_check_permissions(build_id, user, PermissionLevel::Update)
// .await?;
// let server = self.db.get_server(&build.server_id).await?;
// let mut update = Update {
// target: UpdateTarget::Build(build_id.to_string()),
// operation: Operation::RecloneBuild,
// start_ts: monitor_timestamp(),
// status: UpdateStatus::InProgress,
// operator: user.id.clone(),
// success: true,
// ..Default::default()
// };
// update.id = self.add_update(update.clone()).await?;
// update.success = match self.periphery.clone_repo(&server, &build).await {
// Ok(clone_logs) => {
// update.logs.extend(clone_logs);
// true
// }
// Err(e) => {
// update
// .logs
// .push(Log::error("clone repo", format!("{e:#?}")));
// false
// }
// };
// update.status = UpdateStatus::Complete;
// update.end_ts = Some(monitor_timestamp());
// self.update_update(update.clone()).await?;
// Ok(update)
// }
}

View File

@@ -119,10 +119,14 @@ impl State {
.await?;
let start_ts = monitor_timestamp();
let server = self.db.get_server(&deployment.server_id).await?;
let log = self
let log = match self
.periphery
.container_remove(&server, &deployment.name)
.await?;
.await
{
Ok(log) => log,
Err(e) => Log::error("destroy container", format!("{e:#?}")),
};
self.db
.deployments
.delete_one(deployment_id)

View File

@@ -125,7 +125,7 @@ impl State {
} in &new_procedure.stages
{
match operation {
BuildBuild | RecloneBuild => {
BuildBuild => {
self.get_build_check_permissions(&target_id, user, PermissionLevel::Execute)
.await?;
}
@@ -253,13 +253,6 @@ impl State {
.context(format!("failed at build (id: {target_id})"))?;
updates.push(update);
}
RecloneBuild => {
let update = self
.reclone_build(&target_id, user)
.await
.context(format!("failed at reclone build (id: {target_id})"))?;
updates.push(update);
}
// server
PruneImagesServer => {
let update = self.prune_images(&target_id, user).await.context(format!(

View File

@@ -1,6 +1,8 @@
use anyhow::{anyhow, Context};
use diff::Diff;
use futures_util::future::join_all;
use helpers::to_monitor_name;
use mungos::doc;
use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
@@ -102,21 +104,59 @@ impl State {
.get_server_check_permissions(server_id, user, PermissionLevel::Update)
.await?;
let start_ts = monitor_timestamp();
self.db.servers.delete_one(&server_id).await?;
let update = Update {
let mut update = Update {
target: UpdateTarget::Server(server_id.to_string()),
operation: Operation::DeleteServer,
start_ts,
end_ts: Some(monitor_timestamp()),
operator: user.id.clone(),
logs: vec![Log::simple(
"delete server",
format!("deleted server {}", server.name),
)],
success: true,
status: UpdateStatus::InProgress,
..Default::default()
};
self.add_update(update).await?;
update.id = self.add_update(update.clone()).await?;
let res = {
let delete_deployments = self
.db
.deployments
.get_some(doc! { "server_id": server_id }, None)
.await?
.into_iter()
.map(|d| async move { self.delete_deployment(&d.id, user).await });
let delete_builds = self
.db
.builds
.get_some(doc! { "server_id": server_id }, None)
.await?
.into_iter()
.map(|d| async move { self.delete_deployment(&d.id, user).await });
let update_groups = self
.db
.groups
.update_many(doc! {}, doc! { "$pull": { "servers": server_id } });
let (dep_res, build_res, group_res) = tokio::join!(
join_all(delete_deployments),
join_all(delete_builds),
update_groups
);
dep_res.into_iter().collect::<anyhow::Result<Vec<_>>>()?;
build_res.into_iter().collect::<anyhow::Result<Vec<_>>>()?;
group_res?;
self.db.servers.delete_one(&server_id).await?;
anyhow::Ok(())
};
let log = match res {
Ok(_) => Log::simple("delete server", format!("deleted server {}", server.name)),
Err(e) => Log::error("delete server", format!("failed to delete server\n{e:#?}")),
};
update.end_ts = Some(monitor_timestamp());
update.status = UpdateStatus::Complete;
update.success = log.success;
update.logs.push(log);
self.update_update(update).await?;
Ok(server)
}

View File

@@ -7,8 +7,8 @@ use axum::{
use helpers::handle_anyhow_error;
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
use types::{
traits::Permissioned, Build, BuildActionState, BuildVersionsReponse, Operation,
PermissionLevel, UpdateStatus,
traits::Permissioned, AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse,
Operation, PermissionLevel, UpdateStatus,
};
use typeshare::typeshare;
@@ -31,14 +31,12 @@ struct BuildId {
#[derive(Serialize, Deserialize)]
struct CreateBuildBody {
name: String,
server_id: String,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
struct CopyBuildBody {
name: String,
server_id: String,
}
#[typeshare]
@@ -88,7 +86,7 @@ pub fn router() -> Router {
Extension(user): RequestUserExtension,
Json(build): Json<CreateBuildBody>| async move {
let build = state
.create_build(&build.name, build.server_id, &user)
.create_build(&build.name, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(build))
@@ -121,7 +119,7 @@ pub fn router() -> Router {
Json(build): Json<CopyBuildBody>| async move {
let build = spawn_request_action(async move {
state
.copy_build(&id, build.name, build.server_id, &user)
.copy_build(&id, build.name, &user)
.await
.map_err(handle_anyhow_error)
})
@@ -181,23 +179,6 @@ pub fn router() -> Router {
},
),
)
.route(
"/:id/reclone",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(build_id): Path<BuildId>| async move {
let update = spawn_request_action(async move {
state
.reclone_build(&build_id.id, &user)
.await
.map_err(handle_anyhow_error)
})
.await??;
response!(Json(update))
},
),
)
.route(
"/:id/action_state",
get(
@@ -227,6 +208,16 @@ pub fn router() -> Router {
},
),
)
.route(
"/aws_builder_defaults",
get(|Extension(state): StateExtension| async move {
Json(AwsBuilderConfig {
access_key_id: String::new(),
secret_access_key: String::new(),
..state.config.aws.clone()
})
}),
)
}
impl State {

View File

@@ -8,10 +8,11 @@ use axum::{
};
use futures_util::future::join_all;
use helpers::handle_anyhow_error;
use mungos::{Deserialize, Document, Serialize};
use mungos::{doc, options::FindOneOptions, Deserialize, Document, Serialize};
use types::{
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
DockerContainerState, DockerContainerStats, Log, PermissionLevel, Server,
DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server,
UpdateStatus,
};
use typeshare::typeshare;
@@ -297,15 +298,29 @@ pub fn router() -> Router {
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
Path(DeploymentId { id })| async move {
let stats = state
.get_deployment_container_stats(&deployment_id.id, &user)
.get_deployment_container_stats(&id, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(stats))
},
),
)
.route(
"/:id/deployed_version",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(DeploymentId { id })| async move {
let version = state
.get_deployment_deployed_version(&id, &user)
.await
.map_err(handle_anyhow_error)?;
response!(version)
},
),
)
}
impl State {
@@ -443,4 +458,53 @@ impl State {
.await?;
Ok(stats)
}
async fn get_deployment_deployed_version(
&self,
id: &str,
user: &RequestUser,
) -> anyhow::Result<String> {
let deployment = self
.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
.await?;
if deployment.build_id.is_some() {
let latest_deploy_update = self
.db
.updates
.find_one(
doc! {
"target": {
"type": "Deployment",
"id": id
},
"operation": Operation::DeployContainer.to_string(),
"status": UpdateStatus::Complete.to_string(),
"success": true,
},
FindOneOptions::builder().sort(doc! { "_id": -1 }).build(),
)
.await
.context("failed at query to get latest deploy update from mongo")?;
if let Some(update) = latest_deploy_update {
if let Some(version) = update.version {
Ok(version.to_string())
} else {
Ok("latest".to_string())
}
} else {
Ok("latest".to_string())
}
} else {
let split = deployment
.docker_run_args
.image
.split(':')
.collect::<Vec<&str>>();
if let Some(version) = split.get(1) {
Ok(version.to_string())
} else {
Ok("latest".to_string())
}
}
}
}

View File

@@ -74,6 +74,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;
@@ -103,6 +104,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;
@@ -127,6 +129,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;

View File

@@ -33,6 +33,13 @@ struct ModifyUserCreateServerBody {
create_server_permissions: bool,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
struct ModifyUserCreateBuildBody {
user_id: String,
create_build_permissions: bool,
}
pub fn router() -> Router {
Router::new()
.route(
@@ -62,6 +69,15 @@ pub fn router() -> Router {
response!(Json(update))
}),
)
.route(
"/modify_create_build",
post(|state, user, body| async {
let update = modify_user_create_build_permissions(state, user, body)
.await
.map_err(handle_anyhow_error)?;
response!(Json(update))
}),
)
}
async fn update_permissions(
@@ -309,3 +325,58 @@ async fn modify_user_create_server_permissions(
update.id = state.add_update(update.clone()).await?;
Ok(update)
}
async fn modify_user_create_build_permissions(
Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Json(ModifyUserCreateBuildBody {
user_id,
create_build_permissions,
}): Json<ModifyUserCreateBuildBody>,
) -> anyhow::Result<Update> {
if !user.is_admin {
return Err(anyhow!(
"user does not have permissions for this action (not admin)"
));
}
let user = state
.db
.users
.find_one_by_id(&user_id)
.await
.context("failed at mongo query to find target user")?
.ok_or(anyhow!("did not find any user with user_id {user_id}"))?;
state
.db
.users
.update_one::<Document>(
&user_id,
mungos::Update::Set(doc! { "create_build_permissions": create_build_permissions }),
)
.await?;
let update_type = if create_build_permissions {
"enabled"
} else {
"disabled"
};
let ts = monitor_timestamp();
let mut update = Update {
target: UpdateTarget::System,
operation: Operation::ModifyUserCreateBuildPermissions,
logs: vec![Log::simple(
"modify user create build permissions",
format!(
"{update_type} create build permissions for {} (id: {})",
user.username, user.id
),
)],
start_ts: ts.clone(),
end_ts: Some(ts),
status: UpdateStatus::Complete,
success: true,
operator: user.id.clone(),
..Default::default()
};
update.id = state.add_update(update.clone()).await?;
Ok(update)
}

View File

@@ -24,6 +24,7 @@ pub struct RequestUser {
pub id: String,
pub is_admin: bool,
pub create_server_permissions: bool,
pub create_build_permissions: bool,
}
#[derive(Serialize, Deserialize)]
@@ -109,6 +110,7 @@ impl JwtClient {
id: claims.id,
is_admin: user.admin,
create_server_permissions: user.create_server_permissions,
create_build_permissions: user.create_build_permissions,
};
Ok(user)
} else {

View File

@@ -32,9 +32,9 @@ impl State {
let state = State {
db: DbClient::new(config.mongo.clone()).await,
slack: config.slack_url.clone().map(|url| slack::Client::new(&url)),
periphery: PeripheryClient::new(config.passkey.clone()),
config,
update: UpdateWsChannel::new(),
periphery: PeripheryClient::default(),
build_action_states: Default::default(),
deployment_action_states: Default::default(),
server_action_states: Default::default(),

28
docs/builds.md Normal file
View File

@@ -0,0 +1,28 @@
# building images
Monitor builds docker images by cloning the source repository from Github and running ```docker build``` on the configured Dockerfile, which should be present in the source repository.
## repo configuration
Setting related to the github repo are under the *repo* tab on respective build's page.
To specify the github repo to build, just give it the name of the repo and the branch under *github config*. The name is given like ```mbecker20/monitor```, it includes the username / organization that owns the repo.
Many repos are private, in this case a Github access token is required in the periphery.config.toml of the building server. these are specified in the config like ```username = "access_token"```. An account which has access to the repo and is available on the periphery server can be selected to use via the *github account* dropdown menu.
Sometimes a command needs to be run when the repo is cloned, you can configure this in the *on clone* section.
There are two fields to pass for *on clone*. the first is *path*, which changes to working directory. To run the command in the root of the repo, just pass ".". The second field is *command*, this is the shell command to be executed after the repo is cloned.
For example, say your repo had a folder in it called "scripts" with a shell script "on-clone.sh". You would give *path* as "scripts" and command as "sh on-clone.sh". Or you could make *path* just "." and then command would be "sh scripts/on-clone.sh". Either way works fine.
## build configuration
## versioning
Monitor uses a major.minor.patch versioning scheme. Every build will auto increment the patch number, and push the image to docker hub with the version tag as well as the "latest" tag.
[next: deploying](https://github.com/mbecker20/monitor/blob/main/docs/deployments.md)
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

3
docs/deployments.md Normal file
View File

@@ -0,0 +1,3 @@
## deploying applications
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

46
docs/introduction.md Normal file
View File

@@ -0,0 +1,46 @@
# introduction
If you have many servers running many applications, it can be a challenge to keep things organized and easily accessible. Without structure, things can become messy quickly, which means operational issues are more likely to arise and they can take longer to resolve. Ultimately these issues hinder productivity and waste valuable time. Monitor is a web app to provide this structure for how applications are built, deployed, and managed across many servers.
## docker
Monitor is opinionated by design, and [docker](https://docs.docker.com/) is the tool of choice. Docker provides the ability to package applications and their runtime dependencies into a standalone bundle, called an *image*. This makes them easy to "ship" to any server and run without the hassle of setting up the runtime environment. Docker uses the image as a sort of template to create *containers*. Containers are kind of like virtual machines but with different performance characteristics, namely that processes contained still run natively on the system kernel. The file system is seperate though, and like virtual machines, they can be created, started, stopped, and destroyed.
## monitor
Monitor is a solution for handling for the following:
1. Build application source into auto-versioned images.
2. Create, start, stop, and restart Docker containers, and view their status and logs.
3. Keep a record of all the actions that are performed and by whom.
4. View realtime and historical system resource usage.
5. Alerting for server health, like high cpu, memory, disk, etc.
## architecture and components
Monitor is composed of a single core and any amount of connected servers running the periphery application.
### monitor core
The core is a web server that hosts the core API and serves the frontend to be accessed in a web browser. All user interaction with the connected servers flow through the core. It is the stateful part of the system, with the application state stored on an instance of MongoDB.
### monitor periphery
The periphery is a stateless web server that exposes API called by the core. The core calls this API to get system usage and container status / logs, clone git repos, and perform docker actions. It is only intended to be reached from the core, and has an address whitelist to limit the IPs allowed to call this API.
### monitor cli
This is a simple standalone cli that helps perform some actions required to setup monitor core and periphery, like generating config files.
## core API
Monitor exposes powerful functionality over the core's REST API, enabling infrastructure engineers to manage deployments programmatically in addition to with the GUI. There is a [rust crate](https://crates.io/crates/monitor_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests.
## permissioning
Monitor is a system designed to be used by many users, whether they are developers, operations personnel, or administrators. The ability to affect an applications state is very powerful, so monitor has a granular permissioning system to only provide this functionality to the intended users. The permissioning system is explained in detail in the [permissioning](https://github.com/mbecker20/monitor/blob/main/docs/permissions.md) section.
User sign-on is possible using username / password, or with Oauth (Github and Google). Allowed login methods can be configured from the [core config](https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml).
[next: connecting servers](https://github.com/mbecker20/monitor/blob/main/docs/servers.md)
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

1
docs/permissions.md Normal file
View File

@@ -0,0 +1 @@
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

30
docs/servers.md Normal file
View File

@@ -0,0 +1,30 @@
# connecting servers
Integrating a device into the monitor system has 2 steps:
1. Setup and start the periphery agent on the server
2. Adding the server to monitor via the core API
## setup monitor periphery
The easiest way to do this is to follow the [monitor guide](https://github.com/mbecker20/monitor-guide). This is a repo containing directions and scripts enabling command line installation via ssh or remotely.
### manual install steps
1. Download the periphery binary from the latest [release](https://github.com/mbecker20/monitor/releases) or install it using [cargo](https://crates.io/crates/monitor_periphery). If the monitor cli.
2. Create and edit ~/.monitor/periphery.config.toml, following the [config example](https://github.com/mbecker20/monitor/blob/main/config_example/periphery.config.example.toml). The file can be anywhere, it can be passed to periphery via the --config-path flag or with the CONFIG_PATH environment variable. The monitor cli can also be used: ```monitor periphery gen-config```
3. Ensure that inbound connectivity is allowed on the port specified in periphery.config.toml (default 8000).
4. Install docker. Make sure whatever user periphery is run as has access to the docker group without sudo.
5. Start the periphery binary with your preferred process manager, like systemd. The config read from the file is printed on startup, ensure that it is as expected.
## adding the server to monitor
The easiest way to add the server is with the GUI. On the home page, click the + button to the right of the server search bar, configure the name and address of the server. The address is the full http/s url to the periphery server, eg http://12.34.56.78:8000.
Once it is added, you can use access the GUI to modify some config, like the alerting thresholds for cpu, memory and disk usage. A server can also be temporarily disabled, this will prevent alerting if it goes offline.
Since no state is stored on the periphery servers, you can easily redirect all builds / deployments to be hosted on a different server. Just update the address to point to the new server.
[next: building](https://github.com/mbecker20/monitor/blob/main/docs/builds.md)
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

5
docs/setup.md Normal file
View File

@@ -0,0 +1,5 @@
# setting up monitor core
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Rounded_Rectangle_2_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
<g id="Rounded_Rectangle_2">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fceade" d="M15,4H1C0.45,4,0,4.45,0,5v14c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1V5
C16,4.45,15.55,4,15,4z M14,18H2V6h12V18z M19,0H5C4.45,0,4,0.45,4,1v2h2V2h12v12h-1v2h2c0.55,0,1-0.45,1-1V1
C20,0.45,19.55,0,19,0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,113 @@
import { useNavigate } from "@solidjs/router";
import { Component, createSignal, Show } from "solid-js";
import { client, pushNotification } from "..";
import { useAppState } from "../state/StateProvider";
import { Build, Deployment } from "../types";
import { getId } from "../util/helpers";
import { useToggle } from "../util/hooks";
import ConfirmButton from "./shared/ConfirmButton";
import Icon from "./shared/Icon";
import Input from "./shared/Input";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import CenterMenu from "./shared/menu/CenterMenu";
import HoverMenu from "./shared/menu/HoverMenu";
import Selector from "./shared/menu/Selector";
const CopyMenu: Component<{
type: "deployment" | "build";
id: string;
}> = (p) => {
const navigate = useNavigate();
const [show, toggleShow] = useToggle();
const [newName, setNewName] = createSignal("");
const { builds, deployments, servers } = useAppState();
const curr_server = () => {
if (p.type === "build") {
return builds.get(p.id)!.server_id;
} else {
return deployments.get(p.id)!.deployment.server_id;
}
}
const [selectedId, setSelected] = createSignal(curr_server());
const name = () => {
if (p.type === "build") {
return builds.get(p.id)?.name;
} else if (p.type === "deployment") {
return deployments.get(p.id)?.deployment.name;
}
};
const copy = () => {
if (newName().length !== 0) {
let promise: Promise<Build | Deployment>;
if (p.type === "build") {
promise = client.copy_build(p.id, {
name: newName(),
});
} else {
promise = client.copy_deployment(p.id, {
name: newName(),
server_id: selectedId()!,
});
}
toggleShow();
promise.then((val) => {
navigate(`/${p.type}/${getId(val)}`);
});
} else {
pushNotification("bad", "copy name cannot be empty");
}
};
return (
<HoverMenu
target={
<CenterMenu
show={show}
toggleShow={toggleShow}
title={`copy ${p.type} | ${name()}`}
target={<Icon type="duplicate" />}
targetClass="blue"
content={() => (
<Grid placeItems="center">
<Flex alignItems="center">
<Input
placeholder="copy name"
class="card dark"
style={{ padding: "0.5rem" }}
value={newName()}
onEdit={setNewName}
/>
<Show when={p.type === "deployment"}>
<Selector
label="target: "
selected={selectedId()!}
items={servers.ids()!}
onSelect={setSelected}
itemMap={(id) => servers.get(id)!.server.name}
targetClass="blue"
targetStyle={{ display: "flex", gap: "0.5rem" }}
searchStyle={{ width: "100%" }}
position="bottom right"
useSearch
/>
</Show>
</Flex>
<ConfirmButton
class="green"
style={{ width: "100%" }}
onConfirm={copy}
>
copy {p.type}
</ConfirmButton>
</Grid>
)}
position="center"
/>
}
content={`copy ${p.type}`}
position="bottom center"
/>
);
};
export default CopyMenu;

View File

@@ -47,10 +47,10 @@ export const NewDeployment: Component<{ serverID: string }> = (p) => {
);
};
export const NewBuild: Component<{ serverID: string }> = (p) => {
export const NewBuild: Component<{}> = (p) => {
const [showNew, toggleShowNew] = useToggle();
const create = (name: string) => {
client.create_build({ name, server_id: p.serverID });
client.create_build({ name });
};
return (
<Show

View File

@@ -11,31 +11,29 @@ type State = {
const context = createContext<State>();
export const ActionStateProvider: ParentComponent<{}> = (p) => {
export const ActionStateProvider: ParentComponent<{ build_id: string }> = (p) => {
const { ws } = useAppState();
const params = useParams();
const [actions, setActions] = createStore<BuildActionState>({
building: false,
recloning: false,
updating: false,
});
createEffect(() => {
client.get_build_action_state(params.id).then(setActions);
client.get_build_action_state(p.build_id).then(setActions);
});
onCleanup(
ws.subscribe([Operation.BuildBuild], (update) => {
if (update.target.id === params.id) {
if (update.target.id === p.build_id) {
setActions("building", update.status !== UpdateStatus.Complete);
}
})
);
onCleanup(
ws.subscribe([Operation.RecloneBuild], (update) => {
if (update.target.id === params.id) {
setActions("recloning", update.status !== UpdateStatus.Complete);
}
})
);
// onCleanup(
// ws.subscribe([Operation.RecloneBuild], (update) => {
// if (update.target.id === params.id) {
// setActions("recloning", update.status !== UpdateStatus.Complete);
// }
// })
// );
// onCleanup(
// ws.subscribe([DELETE_BUILD], ({ complete, buildID }) => {
// if (buildID === selected.id()) {

View File

@@ -10,20 +10,21 @@ import { useActionStates } from "./ActionStateProvider";
import { client } from "../..";
import { combineClasses, getId } from "../../util/helpers";
import { useParams } from "@solidjs/router";
import { PermissionLevel } from "../../types";
import { PermissionLevel, ServerStatus, ServerWithStatus } from "../../types";
const Actions: Component<{}> = (p) => {
const { user } = useUser();
const params = useParams() as { id: string };
const { builds } = useAppState();
const { builds, servers } = useAppState();
const build = () => builds.get(params.id)!;
const server = () => (build() && build().server_id) ? servers.get(build()!.server_id!) : undefined;
const actions = useActionStates();
const userCanExecute = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Execute ||
build().permissions![getId(user())] === PermissionLevel.Update;
return (
<Show when={userCanExecute()}>
<Show when={userCanExecute() && (server() ? server()?.status === ServerStatus.Ok : true)}>
<Grid class={combineClasses("card shadow")} gridTemplateRows="auto 1fr">
<h1>actions</h1>
<Grid style={{ height: "fit-content" }}>
@@ -47,7 +48,7 @@ const Actions: Component<{}> = (p) => {
</ConfirmButton>
</Show>
</Flex>
<Flex class={combineClasses("action shadow")}>
{/* <Flex class={combineClasses("action shadow")}>
reclone{" "}
<Show
when={!actions.recloning}
@@ -66,7 +67,7 @@ const Actions: Component<{}> = (p) => {
<Icon type="reset" />
</ConfirmButton>
</Show>
</Flex>
</Flex> */}
</Grid>
</Grid>
</Show>

View File

@@ -2,9 +2,7 @@ import { useNavigate, useParams } from "@solidjs/router";
import { Component, createEffect, onCleanup, Show } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { Operation, PermissionLevel } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
import { Operation } from "../../types";
import NotFound from "../NotFound";
import Grid from "../shared/layout/Grid";
import Actions from "./Actions";
@@ -35,7 +33,7 @@ const Build: Component<{}> = (p) => {
onCleanup(() => unsub);
return (
<Show when={build()} fallback={<NotFound type="build" />}>
<ActionStateProvider>
<ActionStateProvider build_id={params.id}>
<Grid
style={{
width: "100%",

View File

@@ -13,6 +13,8 @@ import { A, useParams } from "@solidjs/router";
import { PermissionLevel } from "../../types";
import { client } from "../..";
import HoverMenu from "../shared/menu/HoverMenu";
import CopyMenu from "../CopyMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
const Header: Component<{}> = (p) => {
const { builds, servers } = useAppState();
@@ -25,7 +27,8 @@ const Header: Component<{}> = (p) => {
const userCanUpdate = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Update;
const server = () => servers.get(build().server_id);
const server = () =>
build().server_id ? servers.get(build().server_id!) : undefined;
return (
<>
<Grid
@@ -43,32 +46,39 @@ const Header: Component<{}> = (p) => {
<Flex alignItems="center" justifyContent="space-between">
<h1>{build().name}</h1>
<Show when={userCanUpdate()}>
<HoverMenu
target={
<ConfirmButton
onConfirm={() => {
client.delete_build(params.id);
}}
class="red"
>
<Icon type="trash" />
</ConfirmButton>
}
content="delete build"
position="bottom center"
padding="0.5rem"
/>
<Flex alignItems="center">
<CopyMenu type="build" id={params.id} />
<HoverMenu
target={
<ConfirmMenuButton
onConfirm={() => {
client.delete_build(params.id);
}}
class="red"
title={`delete build | ${build().name}`}
match={build().name}
>
<Icon type="trash" />
</ConfirmMenuButton>
}
content="delete build"
position="bottom center"
padding="0.5rem"
/>
</Flex>
</Show>
</Flex>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center">
<A
href={`/server/${build().server_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{server()?.server.name}
</A>
<Show when={server()} fallback={<div style={{ opacity: 0.7 }}>{build().aws_config ? "aws build" : ""}</div>}>
<A
href={`/server/${build().server_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{server()?.server.name}
</A>
</Show>
<div style={{ opacity: 0.7 }}>build</div>
</Flex>
<div style={{ opacity: 0.7 }}>

View File

@@ -10,7 +10,7 @@ import { createStore, SetStoreFunction } from "solid-js/store";
import { client } from "../../..";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { Build, Operation, PermissionLevel } from "../../../types";
import { Build, Operation, PermissionLevel, ServerWithStatus } from "../../../types";
import { getId } from "../../../util/helpers";
type ConfigBuild = Build & {
@@ -22,6 +22,7 @@ type ConfigBuild = Build & {
type State = {
build: ConfigBuild;
setBuild: SetStoreFunction<ConfigBuild>;
server: () => ServerWithStatus | undefined
reset: () => void;
save: () => void;
userCanUpdate: () => boolean;
@@ -30,7 +31,7 @@ type State = {
const context = createContext<State>();
export const ConfigProvider: ParentComponent<{}> = (p) => {
const { ws, builds } = useAppState();
const { ws, builds, servers } = useAppState();
const params = useParams();
const { user } = useUser();
const [build, set] = createStore({
@@ -44,6 +45,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
set(...args);
set("updated", true);
};
const server = () => build.server_id ? servers.get(build.server_id) : undefined;
const load = () => {
// console.log("load build");
@@ -52,11 +54,11 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
...build,
repo: build.repo,
branch: build.branch,
on_clone: build.on_clone,
pre_build: build.pre_build,
docker_build_args: build.docker_build_args,
docker_account: build.docker_account,
github_account: build.github_account,
aws_config: build.aws_config,
loaded: true,
updated: false,
saving: false,
@@ -105,6 +107,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
const state = {
build,
setBuild,
server,
reset: load,
save,
userCanUpdate,

View File

@@ -2,12 +2,10 @@ import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { PermissionLevel } from "../../../types";
import { getId } from "../../../util/helpers";
import SimpleTabs from "../../shared/tabs/SimpleTabs";
import { Tab } from "../../shared/tabs/Tabs";
import BuildConfig from "./build-config/BuildConfig";
import GitConfig from "./git-config/GitConfig";
import BuilderConfig from "./builder/BuilderConfig";
import BuildConfig from "./config/BuildConfig";
import Owners from "./Permissions";
import { ConfigProvider } from "./Provider";
@@ -24,12 +22,12 @@ const BuildTabs: Component<{}> = (p) => {
tabs={
[
{
title: "repo",
element: () => <GitConfig />,
title: "config",
element: () => <BuildConfig />,
},
{
title: "build",
element: () => <BuildConfig />,
title: "builder",
element: () => <BuilderConfig />
},
user().admin && {
title: "collaborators",

View File

@@ -0,0 +1,116 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const AwsBuilderConfig: Component<{}> = (p) => {
const { build } = useConfig();
return (
<>
<Ami />
<InstanceType />
<VolumeSize />
<Show when={!build.updated}>
<div style={{ height: "4rem" }} />
</Show>
</>
);
};
const Ami: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
const get_ami_id = () => {
if (build.aws_config?.ami_id) {
return build.aws_config.ami_id;
} else {
return aws_builder_config()?.default_ami_id || "unknown";
}
};
const get_ami_name = (ami_id: string) => {
if (aws_builder_config() === undefined || ami_id === "unknown")
return "unknown";
return (
aws_builder_config()!.available_ami_accounts![ami_id]?.name || "unknown"
);
};
const ami_ids = () => {
if (aws_builder_config() === undefined) return [];
return Object.keys(aws_builder_config()!.available_ami_accounts!);
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>ami</h1>
<Selector
targetClass="blue"
selected={get_ami_id()}
items={ami_ids()}
onSelect={(ami_id) => setBuild("aws_config", "ami_id", ami_id)}
itemMap={get_ami_name}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
</Flex>
);
};
const InstanceType: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>instance type</h1>
<Input
placeholder={aws_builder_config()?.default_instance_type}
value={build.aws_config?.instance_type}
onEdit={(instance_type) =>
setBuild("aws_config", "instance_type", instance_type)
}
disabled={!userCanUpdate()}
/>
</Flex>
);
};
const VolumeSize: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>volume size</h1>
<Flex gap="0.25rem" alignItems="center">
<Input
style={{ width: "4rem" }}
placeholder={aws_builder_config()?.default_volume_gb?.toString()}
value={
build.aws_config?.volume_gb
? build.aws_config.volume_gb.toString()
: ""
}
onEdit={(volume_size) =>
setBuild("aws_config", "volume_gb", Number(volume_size))
}
disabled={!userCanUpdate()}
/>
GB
</Flex>
</Flex>
);
};
export default AwsBuilderConfig;

View File

@@ -1,43 +1,27 @@
import { Component, Show } from "solid-js";
import { pushNotification, URL } from "../../../..";
import { combineClasses, copyToClipboard, getId } from "../../../../util/helpers";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Loading from "../../../shared/loading/Loading";
import { useConfig } from "../Provider";
import Git from "./Git";
import OnClone from "./OnClone";
import Loading from "../../../shared/loading/Loading";
import BuilderType from "./BuilderType";
import BuilderServer from "./BuilderServer";
import AwsBuilderConfig from "./AwsBuilderConfig";
const GitConfig: Component<{}> = (p) => {
const BuilderConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
const listenerUrl = () => `${URL}/api/listener/build/${getId(build)}`;
return (
<Show when={build.loaded}>
<Grid class="config">
<Grid class="config-items scroller">
<Git />
<OnClone />
<Show when={userCanUpdate()}>
<Grid class={combineClasses("config-item shadow")}>
<h1>webhook url</h1>
<Flex justifyContent="space-between" alignItems="center">
<div class="ellipsis" style={{ width: "250px" }}>
{listenerUrl()}
</div>
<ConfirmButton
class="blue"
onFirstClick={() => {
copyToClipboard(listenerUrl());
pushNotification("good", "copied url to clipboard");
}}
confirm={<Icon type="check" />}
>
<Icon type="clipboard" />
</ConfirmButton>
</Flex>
</Grid>
<BuilderType />
<Show when={build.server_id}>
<BuilderServer />
<div style={{ height: "12rem" }} />
</Show>
<Show when={build.aws_config}>
<AwsBuilderConfig />
</Show>
</Grid>
<Show when={userCanUpdate() && build.updated}>
@@ -66,4 +50,4 @@ const GitConfig: Component<{}> = (p) => {
);
};
export default GitConfig;
export default BuilderConfig;

View File

@@ -0,0 +1,46 @@
import { Component } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import { PermissionLevel } from "../../../../types";
import { getId } from "../../../../util/helpers";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const BuilderServer: Component<{}> = (p) => {
const { servers, getPermissionOnServer } = useAppState();
const { setBuild, server, userCanUpdate } = useConfig();
const availableServers = () => {
if (!servers.loaded()) return [];
return servers
.ids()!
.filter((id) => {
return getPermissionOnServer(id) === PermissionLevel.Update;
});
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>builder server</h1>
<Selector
targetClass="blue"
selected={server()?.server ? getId(server()!.server) : "select server"}
items={availableServers()}
onSelect={(server_id) => setBuild("server_id", server_id)}
itemMap={(server_id) =>
server_id === "select server"
? "select server"
: servers.get(server_id)!.server.name
}
disabled={!userCanUpdate()}
position="bottom right"
useSearch
/>
</Flex>
);
};
export default BuilderServer;

View File

@@ -0,0 +1,52 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const BuilderType: Component<{}> = (p) => {
const { servers } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
const builderType = () => {
if (build.server_id) {
return "server";
} else if (build.aws_config) {
return "aws";
} else {
return undefined;
}
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>builder type</h1>
<Show when={userCanUpdate()} fallback={<h2>{builderType()}</h2>}>
<Selector
targetClass="blue"
selected={builderType() || "select type"}
items={["aws", "server"]}
position="bottom right"
onSelect={(type) => {
if (type !== builderType()) {
if (type === "server") {
const server_id =
servers.ids()?.length || 0 > 0
? servers.ids()![0]
: undefined;
setBuild({ server_id, aws_config: undefined });
} else if (type === "aws") {
setBuild({ server_id: undefined, aws_config: {} });
}
}
}}
/>
</Show>
</Flex>
);
};
export default BuilderType;

View File

@@ -1,12 +1,10 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import {
combineClasses,
parseDotEnvToEnvVars,
parseEnvVarseToDotEnv,
} from "../../../../util/helpers";
import { useToggle } from "../../../../util/hooks";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import CenterMenu from "../../../shared/menu/CenterMenu";
import TextArea from "../../../shared/TextArea";
import { useConfig } from "../Provider";
@@ -14,24 +12,26 @@ import { useConfig } from "../Provider";
const BuildArgs: Component<{}> = (p) => {
const { build, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<Flex alignItems="center" justifyContent="space-between">
<h1>build args</h1>
<Flex alignItems="center" gap="0.2rem">
<Show
when={
!build.docker_build_args?.build_args ||
build.docker_build_args.build_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<EditBuildArgs />
</Show>
</Flex>
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>build args</h1>
<Flex alignItems="center" gap="0.2rem">
<Show
when={
!build.docker_build_args?.build_args ||
build.docker_build_args.build_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<EditBuildArgs />
</Show>
</Flex>
</Grid>
</Flex>
);
};
@@ -74,7 +74,7 @@ const EditBuildArgs: Component<{}> = (p) => {
value={buildArgs()}
onEdit={setBuildArgs}
style={{
width: "700px",
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",

View File

@@ -9,6 +9,8 @@ import { useConfig } from "../Provider";
import Loading from "../../../shared/loading/Loading";
import BuildArgs from "./BuildArgs";
import Version from "./Version";
import Repo from "./Repo";
import ListenerUrl from "./ListenerUrl";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
@@ -17,9 +19,11 @@ const BuildConfig: Component<{}> = (p) => {
<Grid class="config">
<Grid class="config-items scroller">
<Version />
<Repo />
<Docker />
<BuildArgs />
<CliBuild />
<ListenerUrl />
</Grid>
<Show when={userCanUpdate() && build.updated}>
<Show

View File

@@ -1,6 +1,7 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { ServerStatus } from "../../../../types";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
@@ -9,11 +10,28 @@ import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Docker: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
const { aws_builder_config } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [peripheryDockerAccounts, setPeripheryDockerAccounts] =
createSignal<string[]>();
createEffect(() => {
client.get_server_docker_accounts(build.server_id).then(setDockerAccounts);
if (server()?.status === ServerStatus.Ok) {
client
.get_server_docker_accounts(build.server_id!)
.then(setPeripheryDockerAccounts);
}
});
const dockerAccounts = () => {
if (build.server_id) {
return peripheryDockerAccounts() || [];
} else if (build.aws_config) {
const ami_id =
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
return ami_id
? aws_builder_config()?.available_ami_accounts![ami_id].docker || []
: [];
} else return [];
};
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>docker build</h1> {/* checkbox here? */}
@@ -57,7 +75,7 @@ const Docker: Component<{}> = (p) => {
<Selector
targetClass="blue"
selected={build.docker_account || "none"}
items={["none", ...dockerAccounts()!]}
items={["none", ...dockerAccounts()]}
onSelect={(account) => {
setBuild(
"docker_account",

View File

@@ -0,0 +1,37 @@
import { Component, Show } from "solid-js";
import { pushNotification, URL } from "../../../..";
import { copyToClipboard, getId } from "../../../../util/helpers";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
const ListenerUrl: Component<{}> = (p) => {
const { build, userCanUpdate } = useConfig();
const listenerUrl = () => `${URL}/api/listener/build/${getId(build)}`;
return (
<Show when={userCanUpdate()}>
<Grid class="config-item shadow">
<h1>webhook url</h1>
<Flex justifyContent="space-between" alignItems="center">
<div class="ellipsis" style={{ width: "250px" }}>
{listenerUrl()}
</div>
<ConfirmButton
class="blue"
onFirstClick={() => {
copyToClipboard(listenerUrl());
pushNotification("good", "copied url to clipboard");
}}
confirm={<Icon type="check" />}
>
<Icon type="clipboard" />
</ConfirmButton>
</Flex>
</Grid>
</Show>
);
}
export default ListenerUrl;

View File

@@ -0,0 +1,90 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import { combineClasses } from "../../../../util/helpers";
import { useAppState } from "../../../../state/StateProvider";
import { client } from "../../../..";
import { ServerStatus } from "../../../../types";
import Selector from "../../../shared/menu/Selector";
const Repo: Component<{}> = (p) => {
const { aws_builder_config } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [peripheryGithubAccounts, setPeripheryGithubAccounts] =
createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_github_accounts(build.server_id!)
.then(setPeripheryGithubAccounts);
}
});
const githubAccounts = () => {
if (build.server_id) {
return peripheryGithubAccounts() || [];
} else if (build.aws_config) {
const ami_id =
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
return ami_id
? aws_builder_config()?.available_ami_accounts![ami_id].github || []
: [];
} else return [];
};
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>repo config</h1>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={githubAccounts()}>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.github_account || "none"}
items={["none", ...githubAccounts()]}
onSelect={(account) => {
setBuild(
"github_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>
</Grid>
);
};
export default Repo;

View File

@@ -1,69 +0,0 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import Selector from "../../../shared/menu/Selector";
import { combineClasses } from "../../../../util/helpers";
import { client } from "../../../..";
const Git: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
createEffect(() => {
client.get_server_github_accounts(build.server_id).then(setGithubAccounts)
});
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>github config</h1>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.github_account || "none"}
items={["none", ...githubAccounts()!]}
onSelect={(account) => {
setBuild(
"github_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default Git;

View File

@@ -1,63 +0,0 @@
import { Component } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
const OnClone: Component = () => {
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>on clone</h1>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={build.on_clone?.path || ""}
onEdit={(path) => {
if (
path.length === 0 &&
(!build.on_clone ||
!build.on_clone.command ||
build.on_clone.command.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>command:</h2>
<Input
placeholder="command"
value={build.on_clone?.command || ""}
onEdit={(command) => {
if (
command.length === 0 &&
(!build.on_clone ||
!build.on_clone.path ||
build.on_clone.path.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default OnClone;

View File

@@ -1,5 +1,5 @@
import { Component, Match, Show, Switch } from "solid-js";
import { client, pushNotification } from "../..";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import ConfirmButton from "../shared/ConfirmButton";
@@ -9,24 +9,33 @@ import Grid from "../shared/layout/Grid";
import Loading from "../shared/loading/Loading";
import HoverMenu from "../shared/menu/HoverMenu";
import { useActionStates } from "./ActionStateProvider";
import { combineClasses, getId } from "../../util/helpers";
import { combineClasses } from "../../util/helpers";
import { A, useParams } from "@solidjs/router";
import { DockerContainerState, PermissionLevel } from "../../types";
import {
DockerContainerState,
PermissionLevel,
ServerStatus,
} from "../../types";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
const Actions: Component<{}> = (p) => {
const { deployments, builds, getPermissionOnDeployment } = useAppState();
const { deployments, builds, servers, getPermissionOnDeployment } =
useAppState();
const params = useParams();
const { user, user_id } = useUser();
const deployment = () => deployments.get(params.id)!;
const server = () =>
deployment() && servers.get(deployment()!.deployment.server_id);
const show = () => {
const permissions = getPermissionOnDeployment(params.id);
return (
server()?.status === ServerStatus.Ok &&
deployment() &&
(user().admin ||
permissions === PermissionLevel.Execute ||
permissions === PermissionLevel.Update)
);
};
const deployment = () => deployments.get(params.id)!;
const showBuild = () => {
const build = deployment().deployment.build_id
? builds.get(deployment().deployment.build_id!)
@@ -117,13 +126,17 @@ const Actions: Component<{}> = (p) => {
};
const Build: Component = () => {
const { ws, deployments } = useAppState();
const { deployments } = useAppState();
const params = useParams();
const actions = useActionStates();
const buildID = () => deployments.get(params.id)!.deployment.build_id!;
return (
<Flex class={combineClasses("action shadow")}>
<A href={`/build/${buildID()}`} class="pointer">
<A
href={`/build/${buildID()}`}
class="pointer"
style={{ padding: 0, "font-size": "16px" }}
>
build
</A>
<Show
@@ -152,6 +165,8 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
const params = useParams();
// const deployment = () => deployments.get(params.id)!;
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
return (
<Show
when={!actions.deploying}
@@ -163,14 +178,30 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
>
<HoverMenu
target={
<ConfirmButton
class="green"
onConfirm={() => {
client.deploy_container(params.id);
}}
<Show
when={p.redeploy}
fallback={
<ConfirmButton
class="green"
onConfirm={() => {
client.deploy_container(params.id);
}}
>
<Icon type={"play"} />
</ConfirmButton>
}
>
<Icon type={p.redeploy ? "reset" : "play"} />
</ConfirmButton>
<ConfirmMenuButton
class="green"
onConfirm={() => {
client.deploy_container(params.id);
}}
title={`redeploy container | ${name()}`}
match={name()!}
>
<Icon type={"reset"} />
</ConfirmMenuButton>
</Show>
}
content={p.redeploy ? "redeploy container" : "deploy container"}
position="bottom center"
@@ -183,6 +214,8 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
const RemoveContainer = () => {
const params = useParams();
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
return (
<Show
when={!actions.removing}
@@ -194,14 +227,16 @@ const RemoveContainer = () => {
>
<HoverMenu
target={
<ConfirmButton
<ConfirmMenuButton
class="red"
onConfirm={() => {
client.remove_container(params.id);
}}
title={`destroy container | ${name()}`}
match={name()!}
>
<Icon type="trash" />
</ConfirmButton>
</ConfirmMenuButton>
}
content="delete container"
position="bottom center"
@@ -245,6 +280,8 @@ const Start = () => {
const Stop = () => {
const params = useParams();
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
return (
<Show
when={!actions.stopping}
@@ -256,14 +293,16 @@ const Stop = () => {
>
<HoverMenu
target={
<ConfirmButton
<ConfirmMenuButton
class="orange"
onConfirm={() => {
client.stop_container(params.id);
}}
title={`stop container | ${name()}`}
match={name()!}
>
<Icon type="pause" />
</ConfirmButton>
</ConfirmMenuButton>
}
content="stop container"
position="bottom center"

View File

@@ -1,27 +1,35 @@
import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { MAX_PAGE_WIDTH } from "../..";
import { Component, onCleanup, Show } from "solid-js";
import { client } from "../..";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
import { ServerStatus } from "../../types";
import NotFound from "../NotFound";
import Grid from "../shared/layout/Grid";
import Actions from "./Actions";
import { ActionStateProvider } from "./ActionStateProvider";
import Header from "./Header";
import { ConfigProvider } from "./tabs/config/Provider";
import DeploymentTabs from "./tabs/Tabs";
import Updates from "./Updates";
const Deployment2: Component<{}> = (p) => {
const POLLING_RATE = 10000;
// let interval = -1;
const Deployment: Component<{}> = (p) => {
const { servers, deployments } = useAppState();
const { isSemiMobile } = useAppDimensions();
const params = useParams();
const deployment = () => deployments.get(params.id);
const server = () =>
deployment() && servers.get(deployment()!.deployment.server_id);
// clearInterval(interval);
// interval = setInterval(async () => {
// if (server()?.status === ServerStatus.Ok) {
// const deployment = await client.get_deployment(params.id);
// deployments.update(deployment);
// }
// }, POLLING_RATE);
// onCleanup(() => clearInterval(interval));
return (
<Show
when={deployment() && server()}
@@ -53,46 +61,4 @@ const Deployment2: Component<{}> = (p) => {
);
};
const Deployment: Component<{}> = (p) => {
const { servers, deployments } = useAppState();
const params = useParams();
const deployment = () => deployments.get(params.id);
const server = () => deployment() && servers.get(deployment()!.deployment.server_id);
const { isSemiMobile } = useAppDimensions();
const { user } = useUser();
const userCanUpdate = () => user().admin || deployment()?.deployment.permissions![getId(user())] === PermissionLevel.Update;
return (
<Show
when={deployment() && server()}
fallback={<NotFound type="deployment" />}
>
<ActionStateProvider>
<Grid class={combineClasses("content")}>
{/* left / actions */}
<Grid class="left-content">
<Header />
<Actions />
<Show when={!isSemiMobile() && userCanUpdate()}>
<Updates />
</Show>
</Grid>
{/* right / tabs */}
<Show
when={userCanUpdate()}
fallback={
<h2 class={combineClasses("card tabs shadow")}>
you do not have permission to view this deployment
</h2>
}
>
<ConfigProvider>
<DeploymentTabs />
</ConfigProvider>
</Show>
</Grid>
</ActionStateProvider>
</Show>
);
};
export default Deployment2;
export default Deployment;

View File

@@ -1,12 +1,12 @@
import { Component, Show } from "solid-js";
import { Component, createResource, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import {
combineClasses,
deploymentHeaderStateClass,
getId,
readableVersion,
} from "../../util/helpers";
import ConfirmButton from "../shared/ConfirmButton";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
@@ -17,9 +17,11 @@ import Updates from "./Updates";
import { DockerContainerState, PermissionLevel } from "../../types";
import { A, useParams } from "@solidjs/router";
import { client } from "../..";
import CopyMenu from "../CopyMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
const Header: Component<{}> = (p) => {
const { deployments, servers } = useAppState();
const { deployments, servers, builds } = useAppState();
const params = useParams();
const deployment = () => deployments.get(params.id)!;
const { user } = useUser();
@@ -36,6 +38,27 @@ const Header: Component<{}> = (p) => {
deployment().deployment.permissions![getId(user())] ===
PermissionLevel.Update;
const server = () => servers.get(deployment().deployment.server_id);
const [deployed_version] = createResource(() =>
client.get_deployment_deployed_version(params.id)
);
const image = () => {
if (deployment().deployment.build_id) {
const build = builds.get(deployment().deployment.build_id!)!;
if (deployment().state === DockerContainerState.NotDeployed) {
const version = deployment().deployment.build_version
? readableVersion(deployment().deployment.build_version!).replaceAll(
"v",
""
)
: "latest";
return `${build.name}:${version}`;
} else {
return deployed_version() && `${build.name}:${deployed_version()}`;
}
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
};
return (
<>
<Grid
@@ -51,23 +74,40 @@ const Header: Component<{}> = (p) => {
}}
>
<Flex alignItems="center" justifyContent="space-between">
<h1>{deployment()!.deployment.name}</h1>
<Flex alignItems="center">
<h1>{deployment()!.deployment.name}</h1>
<div style={{ opacity: 0.7 }}>{image()}</div>
</Flex>
<Show when={userCanUpdate()}>
<HoverMenu
target={
<ConfirmButton
onConfirm={() => {
client.delete_deployment(params.id);
}}
class="red"
>
<Icon type="trash" />
</ConfirmButton>
}
content="delete deployment"
position="bottom center"
padding="0.5rem"
/>
<Flex alignItems="center">
<CopyMenu type="deployment" id={params.id} />
<HoverMenu
target={
<ConfirmMenuButton
onConfirm={() => {
client.delete_deployment(params.id);
}}
class="red"
title={`delete deployment | ${
deployment().deployment.name
}`}
match={deployment().deployment.name}
info={
<Show when={deployment().container}>
<div style={{ opacity: 0.7 }}>
warning! this will destroy this deployments container
</div>
</Show>
}
>
<Icon type="trash" />
</ConfirmMenuButton>
}
content="delete deployment"
position="bottom center"
padding="0.5rem"
/>
</Flex>
</Show>
</Flex>
<Flex alignItems="center" justifyContent="space-between">

View File

@@ -17,6 +17,7 @@ import {
DockerContainerState,
Log as LogType,
Operation,
ServerStatus,
} from "../../../types";
import { client } from "../../..";
import SimpleTabs from "../../shared/tabs/SimpleTabs";
@@ -26,18 +27,23 @@ import { useUser } from "../../../state/UserProvider";
const DeploymentTabs: Component<{}> = () => {
const { user } = useUser();
const { deployments, ws } = useAppState();
const { deployments, ws, servers } = useAppState();
const params = useParams();
const deployment = () => deployments.get(params.id);
const server = () =>
deployment() && servers.get(deployment()!.deployment.server_id);
const [logTail, setLogTail] = createSignal(50);
const [log, setLog] = createSignal<LogType>();
const status = () =>
deployment()!.state === DockerContainerState.NotDeployed
? "not deployed"
: deployment()!.container?.state;
// const status = () =>
// deployment()!.state === DockerContainerState.NotDeployed
// ? "not deployed"
// : deployment()!.container?.state;
const log_available = () =>
server()?.status === ServerStatus.Ok &&
deployment()?.state !== DockerContainerState.NotDeployed;
const loadLog = async () => {
console.log("load log");
if (deployment()?.state !== DockerContainerState.NotDeployed) {
if (log_available()) {
console.log("load log");
const log = await client.get_deployment_container_log(
params.id,
logTail()
@@ -78,7 +84,7 @@ const DeploymentTabs: Component<{}> = () => {
title: "config",
element: () => <Config />,
},
status() !== "not deployed" && [
log_available() && [
{
title: "log",
element: () => (
@@ -90,7 +96,7 @@ const DeploymentTabs: Component<{}> = () => {
/>
),
},
status() !== "not deployed" && {
{
title: "error log",
titleElement: () => (
<Flex gap="0.5rem" alignItems="center">

View File

@@ -49,9 +49,9 @@ const Config: Component<{}> = () => {
</Show>
<Network />
<Restart />
<Env />
<Ports />
<Mounts />
<Env />
<ExtraArgs />
<PostImage />
<Show when={isMobile()}>

View File

@@ -12,7 +12,7 @@ import { createStore, SetStoreFunction } from "solid-js/store";
import { client, pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { Deployment, Operation, PermissionLevel } from "../../../../types";
import { Deployment, Operation, PermissionLevel, ServerStatus, ServerWithStatus } from "../../../../types";
import { getId } from "../../../../util/helpers";
type ConfigDeployment = Deployment & {
@@ -25,6 +25,7 @@ type State = {
editing: Accessor<boolean>;
deployment: ConfigDeployment;
setDeployment: SetStoreFunction<ConfigDeployment>;
server: () => ServerWithStatus | undefined;
reset: () => void;
save: () => void;
networks: Accessor<any[]>;
@@ -34,7 +35,7 @@ type State = {
const context = createContext<State>();
export const ConfigProvider: ParentComponent<{}> = (p) => {
const { ws, deployments } = useAppState();
const { ws, deployments, servers } = useAppState();
const params = useParams();
const { user } = useUser();
const [editing] = createSignal(false);
@@ -87,11 +88,13 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
createEffect(load);
const [networks, setNetworks] = createSignal<any[]>([]);
const server = () => servers.get(deployments.get(params.id)!.deployment.server_id);
createEffect(() => {
console.log("load networks");
client
.get_docker_networks(deployments.get(params.id)!.deployment.server_id)
.then(setNetworks);
if (server()?.status === ServerStatus.Ok) {
client
.get_docker_networks(deployments.get(params.id)!.deployment.server_id)
.then(setNetworks);
}
});
const save = () => {
@@ -141,6 +144,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
editing,
deployment,
setDeployment,
server,
reset: load,
save,
networks,

View File

@@ -1,18 +1,20 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import { ServerStatus } from "../../../../../types";
import { combineClasses } from "../../../../../util/helpers";
import Flex from "../../../../shared/layout/Flex";
import Selector from "../../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const DockerAccount: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const { deployment, setDeployment, server, userCanUpdate } = useConfig();
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
createEffect(() => {
client
.get_server_docker_accounts(deployment.server_id)
.then(setDockerAccounts);
if (server()?.status === ServerStatus.Ok) {
client
.get_server_docker_accounts(deployment.server_id)
.then(setDockerAccounts);
}
});
return (
<Flex

View File

@@ -17,7 +17,7 @@ const Env: Component<{}> = (p) => {
<Grid class={combineClasses("config-item shadow")}>
<Flex alignItems="center" justifyContent="space-between">
<h1>environment</h1>
<Flex alignItems="center" gap="0.2rem">
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.environment ||
@@ -74,7 +74,7 @@ const EditDotEnv: Component<{}> = (p) => {
value={dotenv()}
onEdit={setDotEnv}
style={{
width: "700px",
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",

View File

@@ -22,21 +22,11 @@ const ExtraArgs: Component<{}> = (p) => {
<Grid class="config-item shadow">
<Flex justifyContent="space-between" alignItems="center">
<h1>extra args</h1>
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.extra_args ||
deployment.docker_run_args.extra_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={[...deployment.docker_run_args.extra_args!.keys()]}>
{(_, index) => (

View File

@@ -20,6 +20,7 @@ const Network: Component<{}> = (p) => {
onSelect={(network) => setDeployment("docker_run_args", { network })}
position="bottom right"
disabled={!userCanUpdate()}
searchStyle={{ width: "100%", "min-width": "12rem" }}
useSearch
/>
</Flex>

View File

@@ -23,21 +23,11 @@ const Volumes: Component<{}> = (p) => {
<Grid class={combineClasses("config-item shadow")}>
<Flex justifyContent="space-between" alignItems="center">
<h1>volumes</h1>
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.volumes ||
deployment.docker_run_args.volumes.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={deployment.docker_run_args.volumes}>
{({ local, container }, index) => (

View File

@@ -1,5 +1,6 @@
import { Component, createEffect, createSignal } from "solid-js";
import { client } from "../../../../..";
import { ServerStatus } from "../../../../../types";
import { combineClasses } from "../../../../../util/helpers";
import Input from "../../../../shared/Input";
import Flex from "../../../../shared/layout/Flex";
@@ -8,10 +9,14 @@ import Selector from "../../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Git: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const { deployment, server, setDeployment, userCanUpdate } = useConfig();
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
createEffect(() => {
client.get_server_github_accounts(deployment.server_id).then(setGithubAccounts);
if (server()?.status === ServerStatus.Ok) {
client
.get_server_github_accounts(deployment.server_id)
.then(setGithubAccounts);
}
});
return (
<Grid class={combineClasses("config-item shadow")}>

View File

@@ -1,10 +1,17 @@
import { useParams } from "@solidjs/router";
import { Component, createEffect, createSignal, Show } from "solid-js";
import {
Component,
createEffect,
createMemo,
createSignal,
onCleanup,
Show,
} from "solid-js";
import { client, pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { DockerContainerState, Log as LogType } from "../../../../types";
import { combineClasses } from "../../../../util/helpers";
import { useBuffer } from "../../../../util/hooks";
import { useBuffer, useLocalStorageToggle } from "../../../../util/hooks";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
@@ -12,6 +19,10 @@ import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../config/Provider";
import s from "./log.module.scss";
const POLLING_RATE = 5000;
let interval = -1;
const Log: Component<{
log?: LogType;
logTail: number;
@@ -57,9 +68,23 @@ const Log: Component<{
}
};
const buffer = useBuffer(scrolled, 250);
const [poll, togglePoll] = useLocalStorageToggle(
"deployment-log-polling",
false
);
clearInterval(interval);
interval = setInterval(() => {
if (poll() && deployment()?.state === DockerContainerState.Running) {
p.reload();
}
}, POLLING_RATE);
onCleanup(() => clearInterval(interval));
return (
<Show when={p.log}>
<Grid gap="0.5rem" style={{ height: "100%", "grid-template-rows": "auto 1fr" }}>
<Grid
gap="0.5rem"
style={{ height: "100%", "grid-template-rows": "auto 1fr" }}
>
<Flex
alignItems="center"
justifyContent="flex-end"
@@ -100,6 +125,9 @@ const Log: Component<{
>
<Icon type="refresh" />
</button>
<button class={poll() ? "green" : "red"} onClick={togglePoll}>
{poll() ? "" : "don't "}poll
</button>
</Flex>
<div style={{ position: "relative", height: "100%" }}>
<div

View File

@@ -6,6 +6,7 @@ import { useAppState } from "../../state/StateProvider";
import Grid from "../shared/layout/Grid";
import SimpleTabs from "../shared/tabs/SimpleTabs";
import Summary from "./Summary";
import Builds from "./Tree/Builds";
import Groups from "./Tree/Groups";
import { TreeProvider } from "./Tree/Provider";
import Servers from "./Tree/Servers";
@@ -36,6 +37,10 @@ const Home: Component<{}> = (p) => {
title: "servers",
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
},
{
title: "builds",
element: () => <Builds />
}
]}
/>
</TreeProvider>

View File

@@ -0,0 +1,171 @@
import { A } from "@solidjs/router";
import { Component, createMemo, createSignal, For, Show } from "solid-js";
import { client } from "../../..";
import { useAppDimensions } from "../../../state/DimensionProvider";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { PermissionLevel } from "../../../types";
import { getId, readableMonitorTimestamp } from "../../../util/helpers";
import {
ActionStateProvider,
useActionStates,
} from "../../build/ActionStateProvider";
import { NewBuild } from "../../New";
import ConfirmButton from "../../shared/ConfirmButton";
import Icon from "../../shared/Icon";
import Input from "../../shared/Input";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Loading from "../../shared/loading/Loading";
import Selector from "../../shared/menu/Selector";
import { TreeSortType, TREE_SORTS, useTreeState } from "./Provider";
const Builds: Component<{}> = (p) => {
const { user } = useUser();
const { builds } = useAppState();
const { sort, setSort, build_sorter } = useTreeState();
const [buildFilter, setBuildFilter] = createSignal("");
const buildIDs = createMemo(() => {
if (builds.loaded()) {
const filters = buildFilter()
.split(" ")
.filter((term) => term.length > 0)
.map((term) => term.toLowerCase());
return builds
.ids()!
.filter((id) => {
const name = builds.get(id)!.name;
for (const term of filters) {
if (!name.includes(term)) {
return false;
}
}
return true;
})
.sort(build_sorter());
} else {
return undefined;
}
});
return (
<Grid>
<Grid gridTemplateColumns="1fr auto auto">
<Input
placeholder="filter builds"
value={buildFilter()}
onEdit={setBuildFilter}
style={{ width: "100%", padding: "0.5rem" }}
/>
<Selector
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}
position="bottom right"
targetClass="blue"
targetStyle={{ height: "100%" }}
containerStyle={{ height: "100%" }}
/>
<Show when={user().admin || user().create_build_permissions}>
<NewBuild />
</Show>
</Grid>
<For each={buildIDs()}>
{(id) => (
<ActionStateProvider build_id={id}>
<Build id={id} />
</ActionStateProvider>
)}
</For>
</Grid>
);
};
const Build: Component<{ id: string }> = (p) => {
const { isMobile } = useAppDimensions();
const { user } = useUser();
const { builds, servers } = useAppState();
const build = () => builds.get(p.id)!;
const server = () =>
build().server_id ? servers.get(build().server_id!) : undefined;
const version = () => {
return `v${build().version.major}.${build().version.minor}.${
build().version.patch
}`;
};
const lastBuiltAt = () => {
if (
build().last_built_at === undefined ||
build().last_built_at?.length === 0 ||
build().last_built_at === "never"
) {
return "not built";
} else {
return readableMonitorTimestamp(build().last_built_at!);
}
};
const actions = useActionStates();
const userCanExecute = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Execute ||
build().permissions![getId(user())] === PermissionLevel.Update;
const isAwsBuild = () => build().aws_config ? true : false;
return (
<A
href={`/build/${p.id}`}
class="card light shadow"
style={{
width: "100%",
height: "fit-content",
"box-sizing": "border-box",
"justify-content": "space-between",
padding: "0.5rem",
}}
>
<h1 style={{ "font-size": "1.25rem" }}>{build().name}</h1>
<Flex alignItems="center">
<Show when={server()}>
<A
href={`/server/${build().server_id!}`}
style={{ padding: 0, opacity: 0.7 }}
>
<div class="text-hover">{server()?.server.name}</div>
</A>
</Show>
<Show when={isAwsBuild()}>
<div style={{ opacity: 0.7 }}>aws build</div>
</Show>
<h2>{version()}</h2>
<Show when={!isMobile()}>
<div style={{ opacity: 0.7 }}>{lastBuiltAt()}</div>
</Show>
<Show when={userCanExecute()}>
<Show
when={!actions.building}
fallback={
<button
class="green"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Loading type="spinner" />
</button>
}
>
<ConfirmButton
class="green"
onConfirm={() => {
client.build(p.id);
}}
>
<Icon type="build" width="0.9rem" />
</ConfirmButton>
</Show>
</Show>
</Flex>
</A>
);
};
export default Builds;

View File

@@ -6,7 +6,7 @@ export const TREE_SORTS = ["name", "created"] as const;
export type TreeSortType = typeof TREE_SORTS[number];
const value = () => {
const { servers, groups } = useAppState();
const { servers, groups, builds } = useAppState();
const [sort, setSort] = useLocalStorage<TreeSortType>(
TREE_SORTS[0],
"home-sort-v1"
@@ -29,7 +29,7 @@ const value = () => {
}
};
const group_sorter = () => {
if (!groups.loaded) return () => 0;
if (!groups.loaded()) return () => 0;
if (sort() === "name") {
return (a: string, b: string) => {
const ga = groups.get(a)!;
@@ -44,12 +44,30 @@ const value = () => {
} else {
return () => 0;
}
};
const build_sorter = () => {
if (!builds.loaded()) return () => 0;
if (sort() === "name") {
return (a: string, b: string) => {
const ba = builds.get(a)!;
const bb = builds.get(b)!;
if (ba.name < bb.name) {
return -1;
} else if (ba.name > bb.name) {
return 1;
}
return 0;
};
} else {
return () => 0;
}
};
return {
sort,
setSort,
server_sorter,
group_sorter,
build_sorter
};
}

View File

@@ -25,25 +25,27 @@ const Actions: Component<{}> = (p) => {
<Show
when={server() && server().status === ServerStatus.Ok && userCanExecute()}
>
<Grid class={combineClasses("card shadow")}>
<Grid class={combineClasses("card shadow")} gridTemplateRows="auto 1fr">
<h1>actions</h1>
<Flex class={combineClasses("action shadow")}>
prune images <PruneImages />
</Flex>
<Flex class={combineClasses("action shadow")}>
prune containers <PruneContainers />
</Flex>
<Flex class={combineClasses("action shadow")}>
prune networks{" "}
<ConfirmButton
class="green"
onConfirm={() => {
client.prune_docker_networks(params.id);
}}
>
<Icon type="cut" />
</ConfirmButton>
</Flex>
<Grid style={{ height: "fit-content" }}>
<Flex class={combineClasses("action shadow")}>
prune images <PruneImages />
</Flex>
<Flex class={combineClasses("action shadow")}>
prune containers <PruneContainers />
</Flex>
<Flex class={combineClasses("action shadow")}>
prune networks{" "}
<ConfirmButton
class="green"
onConfirm={() => {
client.prune_docker_networks(params.id);
}}
>
<Icon type="cut" />
</ConfirmButton>
</Flex>
</Grid>
</Grid>
</Show>
);

View File

@@ -9,10 +9,12 @@ import Grid from "../shared/layout/Grid";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useLocalStorageToggle } from "../../util/hooks";
import Updates from "./Updates";
import { PermissionLevel, Server } from "../../types";
import { PermissionLevel, ServerStatus } from "../../types";
import { A, useParams } from "@solidjs/router";
import { client } from "../..";
import Loading from "../shared/loading/Loading";
import HoverMenu from "../shared/menu/HoverMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
const Header: Component<{}> = (p) => {
const { servers } = useAppState();
@@ -26,25 +28,73 @@ const Header: Component<{}> = (p) => {
const userCanUpdate = () =>
user().admin ||
server().server.permissions![getId(user())] === PermissionLevel.Update;
const [version] = createResource(async () => {
return await client.get_server_version(params.id).catch();
});
const [version] = createResource(
() => server() && server().status === ServerStatus.Ok,
async (do_it?: boolean) => {
if (!do_it) return;
return await client.get_server_version(params.id).catch();
}
);
return (
<>
<Flex
<Grid
gap="0.5rem"
class={combineClasses("card shadow")}
justifyContent="space-between"
alignItems="center"
style={{
position: "relative",
cursor: isSemiMobile() ? "pointer" : undefined,
height: "fit-content",
}}
onClick={() => {
if (isSemiMobile()) toggleShowUpdates();
}}
>
<Grid gap="0.1rem">
<Flex alignItems="center" justifyContent="space-between">
<h1>{server().server.name}</h1>
<Show when={userCanUpdate()}>
<Flex alignItems="center">
<div class={serverStatusClass(server().status)}>{status()}</div>
<HoverMenu
target={
<A
href={`/server/${params.id}/stats`}
class="blue"
onClick={(e) => e.stopPropagation()}
>
<Icon type="timeline-line-chart" />
</A>
}
content="server stats"
position="bottom center"
padding="0.5rem"
/>
<HoverMenu
target={
<ConfirmMenuButton
onConfirm={() => {
client.delete_server(params.id);
}}
class="red"
title={`delete server | ${server().server.name}`}
match={server().server.name}
info={
<div style={{ opacity: 0.7 }}>
warning! this will also delete all builds and
deployments on this server
</div>
}
>
<Icon type="trash" />
</ConfirmMenuButton>
}
content="delete server"
position="bottom center"
padding="0.5rem"
/>
</Flex>
</Show>
</Flex>
<Flex alignItems="center" justifyContent="space-between">
<Flex gap="0.2rem" alignItems="center" style={{ opacity: 0.8 }}>
<div>server</div>
<Show when={server().server.region}>
@@ -52,42 +102,13 @@ const Header: Component<{}> = (p) => {
{server().server.region}
</Show>
</Flex>
</Grid>
<Flex alignItems="center">
<Show when={!isMobile()}>
<Show when={version()} fallback={<Loading type="three-dot" />}>
<Show when={version()}>
<div style={{ opacity: 0.7 }}>periphery v{version()}</div>
</Show>
</Show>
<div class={serverStatusClass(server().status)}>{status()}</div>
<A
href={`/server/${params.id}/stats`}
class="blue"
onClick={(e) => e.stopPropagation()}
>
<Icon type="timeline-line-chart" />
</A>
<Show when={userCanUpdate()}>
<ConfirmButton
onConfirm={() => {
client.delete_server(params.id);
}}
class="red"
>
<Icon type="trash" />
</ConfirmButton>
</Show>
</Flex>
<Show when={isSemiMobile()}>
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
updates{" "}
<Icon
type={showUpdates() ? "chevron-up" : "chevron-down"}
width="0.9rem"
/>
</Flex>
</Show>
</Flex>
</Grid>
<Show when={isSemiMobile() && showUpdates()}>
<Updates />
</Show>

View File

@@ -32,7 +32,7 @@ const Server: Component<{}> = (p) => {
style={{ width: "100%" }}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<Grid>
<Grid style={{ "flex-grow": 1, "grid-auto-rows": "auto 1fr" }}>
<Header />
<Actions />
</Grid>

View File

@@ -2,6 +2,7 @@ import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { ServerStatus } from "../../../types";
import SimpleTabs from "../../shared/tabs/SimpleTabs";
import { Tab } from "../../shared/tabs/Tabs";
import Config from "./config/Config";
@@ -26,7 +27,7 @@ const ServerTabs: Component<{}> = (p) => {
title: "config",
element: () => <Config />,
},
{
server()?.status === ServerStatus.Ok && {
title: "info",
element: () => <Info />
},

View File

@@ -12,7 +12,7 @@ import { createStore, SetStoreFunction } from "solid-js/store";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { Server, Operation, PermissionLevel } from "../../../../types";
import { Server, Operation, PermissionLevel, ServerStatus } from "../../../../types";
import { getId } from "../../../../util/helpers";
type ConfigServer = Server & { loaded: boolean; updated: boolean };
@@ -59,7 +59,9 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
const [networks, setNetworks] = createSignal<any[]>([]);
const loadNetworks = () => {
// console.log("load networks");
client.get_docker_networks(params.id).then(setNetworks);
if (servers.get(params.id)?.status === ServerStatus.Ok) {
client.get_docker_networks(params.id).then(setNetworks);
}
};
createEffect(loadNetworks);

View File

@@ -1,23 +1,50 @@
import { A } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { Component, createResource, Show } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { combineClasses, deploymentStateClass, getId } from "../../util/helpers";
import { DockerContainerState } from "../../types";
import {
combineClasses,
deploymentStateClass,
getId,
readableVersion,
} from "../../util/helpers";
import Circle from "../shared/Circle";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import s from "./serverchildren.module.scss";
const Deployment: Component<{ id: string }> = (p) => {
const { deployments } = useAppState();
const { deployments, builds } = useAppState();
const deployment = () => deployments.get(p.id)!;
const [deployed_version] = createResource(() =>
client.get_deployment_deployed_version(p.id)
);
const image = () => {
if (deployment().deployment.build_id) {
const build = builds.get(deployment().deployment.build_id!)!;
if (deployment().state === DockerContainerState.NotDeployed) {
const version = deployment().deployment.build_version
? readableVersion(deployment().deployment.build_version!).replaceAll(
"v",
""
)
: "latest";
return `${build.name}:${version}`;
} else {
return deployed_version() && `${build.name}:${deployed_version()}`;
}
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
};
return (
<Show when={deployment()}>
<A
href={`/deployment/${p.id}`}
class={combineClasses(
s.DropdownItem,
)}
>
<h2>{deployment().deployment.name}</h2>
<A href={`/deployment/${p.id}`} class={combineClasses(s.DropdownItem)}>
<Grid gap="0">
<h2>{deployment().deployment.name}</h2>
<div style={{ opacity: 0.7 }}>{image()}</div>
</Grid>
<Flex alignItems="center">
<div style={{ opacity: 0.7 }}>{deployments.status(p.id)}</div>
<Circle

View File

@@ -6,15 +6,14 @@ import SimpleTabs from "../shared/tabs/SimpleTabs";
import s from "./serverchildren.module.scss";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { NewBuild, NewDeployment } from "../New";
import { NewDeployment } from "../New";
import Deployment from "./Deployment";
import Build from "./Build";
import { useAppState } from "../../state/StateProvider";
const ServerChildren: Component<{ id: string }> = (p) => {
const { user } = useUser();
const { isSemiMobile } = useAppDimensions();
const { servers, deployments, builds } = useAppState();
const { servers, deployments } = useAppState();
const server = () => servers.get(p.id);
const deploymentIDs = createMemo(() => {
return (deployments.loaded() &&
@@ -24,61 +23,79 @@ const ServerChildren: Component<{ id: string }> = (p) => {
(id) => deployments.get(id)?.deployment.server_id === p.id
)) as string[];
});
const buildIDs = createMemo(() => {
return (builds.loaded() &&
builds
.ids()!
.filter((id) => builds.get(id)?.server_id === p.id)) as string[];
});
// const buildIDs = createMemo(() => {
// return (builds.loaded() &&
// builds
// .ids()!
// .filter((id) => builds.get(id)?.server_id === p.id)) as string[];
// });
return (
<SimpleTabs
containerClass="card shadow"
localStorageKey={`${p.id}-home-tab`}
tabs={[
{
title: "deployments",
element: () => (
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
</Show>
</Grid>
),
},
{
title: "builds",
element: () => (
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={buildIDs()}>{(id) => <Build id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewBuild serverID={p.id} />
</Show>
</Grid>
),
},
]}
/>
<div class="card shadow">
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
</Show>
</Grid>
</div>
// <SimpleTabs
// containerClass="card shadow"
// localStorageKey={`${p.id}-home-tab`}
// tabs={[
// {
// title: "deployments",
// element: () => (
// <Grid
// gap=".5rem"
// class={combineClasses(s.Deployments)}
// gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
// >
// <For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
// <Show
// when={
// user().admin ||
// server()?.server.permissions![getId(user())] ===
// PermissionLevel.Update
// }
// >
// <NewDeployment serverID={p.id} />
// </Show>
// </Grid>
// ),
// },
// // {
// // title: "builds",
// // element: () => (
// // <Grid
// // gap=".5rem"
// // class={combineClasses(s.Deployments)}
// // gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
// // >
// // <For each={buildIDs()}>{(id) => <Build id={id} />}</For>
// // <Show
// // when={
// // user().admin ||
// // server()?.server.permissions![getId(user())] ===
// // PermissionLevel.Update
// // }
// // >
// // <NewBuild serverID={p.id} />
// // </Show>
// // </Grid>
// // ),
// // },
// ]}
// />
);
};

View File

@@ -17,6 +17,7 @@ const ConfirmButton: Component<{
onBlur={() => set(false)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (confirm()) {
p.onConfirm && p.onConfirm();
} else {

View File

@@ -0,0 +1,83 @@
import { Component, createSignal, JSX } from "solid-js";
import { pushNotification } from "../..";
import { useToggle } from "../../util/hooks";
import ConfirmButton from "./ConfirmButton";
import Input from "./Input";
import Grid from "./layout/Grid";
import CenterMenu from "./menu/CenterMenu";
const ConfirmMenuButton: Component<{
onConfirm?: () => void;
class?: string;
style?: JSX.CSSProperties;
title: string;
match: string;
info?: JSX.Element;
children: JSX.Element;
}> = (p) => {
const [show, toggleShow] = useToggle();
return (
<CenterMenu
show={show}
toggleShow={toggleShow}
title={p.title}
targetClass={p.class}
target={p.children}
content={() => (
<ConfirmMenuContent
class={p.class}
title={p.title}
match={p.match}
info={p.info}
onConfirm={p.onConfirm}
/>
)}
position="center"
/>
);
};
const ConfirmMenuContent: Component<{
class?: string;
title: string;
match: string;
onConfirm?: () => void;
info?: JSX.Element;
}> = (p) => {
const [input, setInput] = createSignal("");
return (
<Grid placeItems="center">
{p.info}
<Input
class="darkgrey"
style={{
padding: "0.5rem",
width: "100%",
"border-style": "solid",
"border-width": "1px",
"border-color": input() === p.match ? "#41764c" : "#952E23",
}}
placeholder={`enter '${p.match}'`}
onEdit={setInput}
value={input()}
autofocus
/>
<ConfirmButton
class={p.class}
style={{ width: "100%" }}
onConfirm={() => {
if (input() === p.match) {
p.onConfirm && p.onConfirm();
} else {
pushNotification("bad", "must enter value to confirm");
}
}}
>
{p.title}
</ConfirmButton>
</Grid>
);
};
export default ConfirmMenuButton;

View File

@@ -45,7 +45,8 @@ export type IconType =
| "cog"
| "home"
| "timeline-line-chart"
| "arrow-right";
| "arrow-right"
| "duplicate";
const ICON_DIR = import.meta.env.VITE_ICON_DIR || "/assets/icons"

View File

@@ -1,8 +1,6 @@
import {
Accessor,
Component,
createEffect,
createSignal,
JSX,
JSXElement,
Show,
@@ -27,16 +25,16 @@ const CenterMenu: Component<{
style?: JSX.CSSProperties;
position?: "top" | "center";
}> = (p) => {
const [buffer, set] = createSignal(p.show());
createEffect(() => {
if (p.show()) {
set(true);
} else {
setTimeout(() => {
set(false);
}, 350);
}
});
// const [buffer, set] = createSignal(p.show());
// createEffect(() => {
// if (p.show()) {
// set(true);
// } else {
// setTimeout(() => {
// set(false);
// }, 350);
// }
// });
return (
<>
<button
@@ -49,7 +47,7 @@ const CenterMenu: Component<{
>
{p.target}
</button>
<Show when={buffer()}>
<Show when={p.show()}>
<Child {...p} show={p.show} toggleShow={p.toggleShow} />
</Show>
</>
@@ -69,7 +67,7 @@ const Child: Component<{
useKeyDown("Escape", p.toggleShow);
return (
<Grid
class={combineClasses(s.CenterMenuContainer, p.show() ? s.Enter : s.Exit)}
class={combineClasses(s.CenterMenuContainer)}
onClick={(e) => {
e.stopPropagation();
p.toggleShow();

View File

@@ -22,18 +22,18 @@ const HoverMenu: Component<{
containerStyle?: JSX.CSSProperties;
}> = (p) => {
const [show, set] = createSignal(false);
const [buffer, setBuffer] = createSignal(false);
let timeout: number;
createEffect(() => {
clearTimeout(timeout);
if (show()) {
setBuffer(true);
} else {
timeout = setTimeout(() => {
setBuffer(false);
}, 350);
}
});
// const [buffer, setBuffer] = createSignal(false);
// let timeout: number;
// createEffect(() => {
// clearTimeout(timeout);
// if (show()) {
// setBuffer(true);
// } else {
// timeout = setTimeout(() => {
// setBuffer(false);
// }, 350);
// }
// });
return (
<Flex
class={s.HoverMenuTarget}
@@ -44,13 +44,13 @@ const HoverMenu: Component<{
alignItems="center"
>
{p.target}
<Show when={buffer()}>
<Show when={show()}>
<div
class={combineClasses(
p.contentClass,
getPositionClass(p.position),
s.HoverMenu,
show() ? s.Enter : s.Exit,
// show() ? s.Enter : s.Exit,
)}
onMouseOut={() => {
set(false);
@@ -59,7 +59,7 @@ const HoverMenu: Component<{
set(false)
e.stopPropagation();
}}
style={{ ...p.contentStyle, padding: p.padding }}
style={{ ...p.contentStyle, padding: p.padding || "0.5rem" }}
>
{p.content}
</div>

View File

@@ -1,7 +1,5 @@
import {
Component,
createEffect,
createSignal,
JSX,
JSXElement,
Show,
@@ -22,20 +20,20 @@ const Menu: Component<{
containerStyle?: JSX.CSSProperties;
backgroundColor?: string;
}> = (p) => {
const [buffer, set] = createSignal(p.show);
createEffect(() => {
if (p.show) {
set(true);
} else {
setTimeout(() => {
set(false);
}, 350);
}
});
// const [buffer, set] = createSignal(p.show);
// createEffect(() => {
// if (p.show) {
// set(true);
// } else {
// setTimeout(() => {
// set(false);
// }, 350);
// }
// });
return (
<div class={s.MenuContainer} style={p.containerStyle}>
{p.target}
<Show when={buffer()}>
<Show when={p.show}>
<div
class={s.MenuBackground}
style={{ "background-color": p.backgroundColor }}
@@ -47,7 +45,7 @@ const Menu: Component<{
s.Menu,
"shadow",
getPositionClass(p.position),
p.show ? s.Enter : s.Exit
// p.show ? s.Enter : s.Exit
)}
style={{ padding: p.padding as any, ...p.menuStyle }}
onClick={(e) => e.stopPropagation()}

View File

@@ -33,10 +33,12 @@ const Selector: Component<{
itemClass?: string;
itemStyle?: JSX.CSSProperties;
label?: JSXElement;
itemMap?: (item: string) => string;
}> = (p) => {
const [show, toggle] = useToggle();
const [search, setSearch] = createSignal("");
let ref: HTMLInputElement | undefined;
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected);
createEffect(() => {
if (show()) setTimeout(() => ref?.focus(), 200);
});
@@ -46,7 +48,7 @@ const Selector: Component<{
fallback={
<div class={p.disabledClass} style={p.disabledStyle}>
<Show when={p.label}>{p.label}</Show>
{p.selected}
{current()}
</div>
}
>
@@ -60,7 +62,7 @@ const Selector: Component<{
target={
<button class={p.targetClass} onClick={toggle} style={p.targetStyle}>
<Show when={p.label}>{p.label}</Show>
{p.selected}
{current()}
<Icon type="chevron-down" />
</button>
}
@@ -84,7 +86,11 @@ const Selector: Component<{
<For
each={
p.useSearch
? p.items.filter((item) => item.includes(search()))
? p.items.filter((item) =>
p.itemMap
? p.itemMap(item).includes(search())
: item.includes(search())
)
: p.items
}
>
@@ -101,7 +107,7 @@ const Selector: Component<{
}}
class={combineClasses(p.itemClass, s.SelectorItem)}
>
{item}
{p.itemMap ? p.itemMap(item) : item}
</button>
)}
</For>

View File

@@ -16,7 +16,6 @@ const Account: Component<{ close: () => void }> = (p) => {
<Show when={isMobile()}>
<Flex justifyContent="center">{user().username}</Flex>
</Show>
<Flex justifyContent="center">admin: {user().admin.toString()}</Flex>
<Show when={user().admin}>
<A
href="/users"
@@ -27,12 +26,12 @@ const Account: Component<{ close: () => void }> = (p) => {
manage users
</A>
</Show>
<Show when={!user().admin}>
{/* <Show when={!user().admin}>
<Flex justifyContent="center">
create server permissions:{" "}
{user().create_server_permissions.toString()}
{user().create_server_permissions?.toString()}
</Flex>
</Show>
</Show> */}
<A
href="/account"
class="grey"

View File

@@ -17,7 +17,7 @@ import { ControlledTabs } from "../../shared/tabs/Tabs";
import { useAppDimensions } from "../../../state/DimensionProvider";
import Grid from "../../shared/layout/Grid";
import { A, useNavigate } from "@solidjs/router";
import { ServerStatus } from "../../../types";
import { Build, ServerStatus } from "../../../types";
const mobileStyle: JSX.CSSProperties = {
// position: "fixed",
@@ -191,8 +191,10 @@ const Builds: Component<{ close: () => void }> = (p) => {
gap="0.2rem"
style={{ opacity: 0.6, "font-size": "0.9rem" }}
>
{servers.get(build.server_id)?.server.name}
<Icon type="caret-right" width="0.7rem" />
<Show when={build.server_id}>
{build.server_id && servers.get(build.server_id)?.server.name}
<Icon type="caret-right" width="0.7rem" />
</Show>
build
</Flex>
</Grid>

View File

@@ -52,7 +52,7 @@ export default UpdateMenu;
const UpdateMenuContent: Component<{ update: UpdateType }> = (p) => {
return (
<Grid class={s.LogContainer}>
<Grid class={combineClasses(s.LogContainer, "scroller")}>
<UpdateSummary update={p.update} />
<UpdateLogs update={p.update} />
</Grid>
@@ -66,7 +66,7 @@ const UpdateSummary: Component<{ update: UpdateType }> = (p) => {
gap="0.5rem"
class="card light shadow"
gridTemplateColumns="1fr 1fr"
style={{ width: "40rem", "max-width": "90vw" }}
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Flex gap="0.5rem" alignItems="center">
status: <h2>{p.update.status.replaceAll("_", " ")}</h2>

View File

@@ -41,7 +41,6 @@
.LogContainer {
max-height: 80vh;
overflow: scroll;
}
.Log {
@@ -49,8 +48,8 @@
overflow-wrap: anywhere;
/* word-wrap: break-word; */
tab-size: 2;
width: 40rem;
max-width: 90vw;
width: 1000px;
max-width: 80vw;
box-sizing: border-box;
background-color: rgba(c.$darkgrey, 0.6);
padding: 1rem;

View File

@@ -8,6 +8,7 @@ import {
Show,
} from "solid-js";
import { client } from "../..";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { Operation } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
@@ -18,9 +19,19 @@ import Loading from "../shared/loading/Loading";
import s from "./users.module.scss";
const Users: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const { ws } = useAppState();
const [users, { refetch }] = createResource(() => client.list_users());
onCleanup(ws.subscribe([Operation.ModifyUserEnabled], refetch));
onCleanup(
ws.subscribe(
[
Operation.ModifyUserEnabled,
Operation.ModifyUserCreateServerPermissions,
Operation.ModifyUserCreateBuildPermissions,
],
refetch
)
);
const [search, setSearch] = createSignal("");
const filteredUsers = createMemo(() =>
users()?.filter((user) => user.username.includes(search()))
@@ -34,55 +45,78 @@ const Users: Component<{}> = (p) => {
</Grid>
}
>
<Grid class={s.UsersContent}>
<Grid class={combineClasses(s.Users, "card shadow")}>
<Flex justifyContent="space-between">
<h1>users</h1>
<Input
class="lightgrey"
placeholder="search"
value={search()}
onEdit={setSearch}
/>
</Flex>
<For each={filteredUsers()}>
{(user) => (
<Flex class={combineClasses(s.User, "shadow")}>
<div class={s.Username}>{user.username}</div>
<Flex alignItems="center">
<button
class={user.enabled ? "green" : "red"}
onClick={() => {
client.modify_user_enabled({
user_id: getId(user),
enabled: !user.enabled,
});
}}
>
{user.enabled ? "enabled" : "disabled"}
</button>
<button
class={user.create_server_permissions ? "green" : "red"}
onClick={() => {
client.modify_user_create_server_permissions({
user_id: getId(user),
create_server_permissions: !user.create_server_permissions,
});
}}
>
{user.create_server_permissions ? "can create servers" : "cannot create servers"}
</button>
{/* <ConfirmButton
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Flex justifyContent="space-between">
<h1>users</h1>
<Input
class="lightgrey"
placeholder="search"
value={search()}
onEdit={setSearch}
/>
</Flex>
<For each={filteredUsers()}>
{(user) => (
<Flex class={combineClasses(s.User, "shadow")}>
<div class={s.Username}>{user.username}</div>
<Grid
placeItems="center end"
gridTemplateColumns={!isMobile() ? "1fr 1fr 1fr" : undefined}
>
<button
class={user.enabled ? "green" : "red"}
style={{ width: isMobile() ? "11rem" : "6rem" }}
onClick={() => {
client.modify_user_enabled({
user_id: getId(user),
enabled: !user.enabled,
});
}}
>
{user.enabled ? "enabled" : "disabled"}
</button>
<button
class={user.create_server_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={() => {
client.modify_user_create_server_permissions({
user_id: getId(user),
create_server_permissions:
!user.create_server_permissions,
});
}}
>
{user.create_server_permissions
? "can create servers"
: "cannot create servers"}
</button>
<button
class={user.create_build_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={() => {
client.modify_user_create_build_permissions({
user_id: getId(user),
create_build_permissions: !user.create_build_permissions,
});
}}
>
{user.create_build_permissions
? "can create builds"
: "cannot create builds"}
</button>
{/* <ConfirmButton
class="red"
onConfirm={() => deleteUser(user._id!)}
>
<Icon type="trash" />
</ConfirmButton> */}
</Flex>
</Flex>
)}
</For>
</Grid>
</Grid>
</Flex>
)}
</For>
</Grid>
</Show>
);

View File

@@ -1,5 +1,5 @@
import { useNavigate } from "@solidjs/router";
import { createContext, ParentComponent, useContext } from "solid-js";
import { createContext, createResource, ParentComponent, Resource, useContext } from "solid-js";
import { useWindowKeyDown } from "../util/hooks";
import {
useBuilds,
@@ -14,7 +14,8 @@ import {
} from "./hooks";
import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { PermissionLevel } from "../types";
import { AwsBuilderConfig, PermissionLevel } from "../types";
import { client } from "..";
export type State = {
usernames: ReturnType<typeof useUsernames>;
@@ -32,6 +33,7 @@ export type State = {
procedures: ReturnType<typeof useProcedures>;
getPermissionOnProcedure: (id: string) => PermissionLevel;
updates: ReturnType<typeof useUpdates>;
aws_builder_config: Resource<AwsBuilderConfig>;
};
const context = createContext<
@@ -51,6 +53,7 @@ export const AppStateProvider: ParentComponent = (p) => {
const procedures = useProcedures();
const deployments = useDeployments();
const usernames = useUsernames();
const [aws_builder_config] = createResource(() => client.get_aws_builder_defaults());
const state: State = {
usernames,
servers,
@@ -102,8 +105,8 @@ export const AppStateProvider: ParentComponent = (p) => {
return PermissionLevel.None;
}
},
serverStats: useServerStats(),
serverInfo: useServerInfo(),
serverStats: useServerStats(servers),
serverInfo: useServerInfo(servers),
groups,
getPermissionOnGroup: (id: string) => {
const group = groups.get(id)!;
@@ -129,6 +132,7 @@ export const AppStateProvider: ParentComponent = (p) => {
}
},
updates: useUpdates(),
aws_builder_config,
};
// createEffect(() => {

View File

@@ -47,13 +47,19 @@ export function useServers() {
);
}
export function useServerStats() {
export function useServerStats(servers: ReturnType<typeof useServers>) {
const [stats, set] = createSignal<Record<string, SystemStats | undefined>>(
{}
);
const load = async (serverID: string) => {
const stats = await client.get_server_stats(serverID);
set((s) => ({ ...s, [serverID]: stats }));
if (servers.get(serverID)?.status === ServerStatus.Ok) {
try {
const stats = await client.get_server_stats(serverID);
set((s) => ({ ...s, [serverID]: stats }));
} catch (error) {
console.log("error getting server stats");
}
}
};
const loading: Record<string, boolean> = {};
setTimeout(() => Object.keys(stats()).forEach(load), 30000);
@@ -74,13 +80,19 @@ export function useServerStats() {
};
}
export function useServerInfo() {
export function useServerInfo(servers: ReturnType<typeof useServers>) {
const [info, set] = createSignal<
Record<string, SystemInformation | undefined>
>({});
const load = async (serverID: string) => {
const info = await client.get_server_system_info(serverID);
set((s) => ({ ...s, [serverID]: info }));
if (servers.get(serverID)?.status === ServerStatus.Ok) {
try {
const info = await client.get_server_system_info(serverID);
set((s) => ({ ...s, [serverID]: info }));
} catch (error) {
console.log("error getting server info", error);
}
}
};
const loading: Record<string, boolean> = {};
return {

View File

@@ -11,9 +11,12 @@ body {
background-color: c.$darkgrey;
color: c.$app-color;
scrollbar-color: rgba(252, 234, 222, 0.3) transparent;
scrollbar-width: thin;
display: grid;
grid-template-columns: 1fr;
place-items: center;
place-items: center;
}
code {

View File

@@ -2,6 +2,8 @@
Generated by typeshare 1.0.0
*/
export type AvailableAmiAccounts = Record<string, AmiAccounts>;
export type PermissionsMap = Record<string, PermissionLevel>;
export interface Action {
@@ -21,12 +23,12 @@ export interface Build {
_id?: string;
name: string;
permissions?: PermissionsMap;
server_id: string;
server_id?: string;
aws_config?: AwsBuilderBuildConfig;
version: Version;
repo?: string;
branch?: string;
github_account?: string;
on_clone?: Command;
pre_build?: Command;
docker_build_args?: DockerBuildArgs;
docker_account?: string;
@@ -37,7 +39,6 @@ export interface Build {
export interface BuildActionState {
building: boolean;
recloning: boolean;
updating: boolean;
}
@@ -58,6 +59,37 @@ export interface BuildVersionsReponse {
ts: string;
}
export interface AwsBuilderBuildConfig {
region?: string;
instance_type?: string;
ami_id?: string;
volume_gb?: number;
subnet_id?: string;
security_group_ids?: string[];
key_pair_name?: string;
assign_public_ip?: boolean;
}
export interface AwsBuilderConfig {
access_key_id: string;
secret_access_key: string;
default_ami_id: string;
default_subnet_id: string;
default_key_pair_name: string;
available_ami_accounts?: AvailableAmiAccounts;
default_region?: string;
default_volume_gb?: number;
default_instance_type?: string;
default_security_group_ids?: string[];
default_assign_public_ip?: boolean;
}
export interface AmiAccounts {
name: string;
github?: string[];
docker?: string[];
}
export interface Deployment {
_id?: string;
name: string;
@@ -327,9 +359,10 @@ export interface Log {
export interface User {
_id?: string;
username: string;
enabled: boolean;
admin: boolean;
create_server_permissions: boolean;
enabled?: boolean;
admin?: boolean;
create_server_permissions?: boolean;
create_build_permissions?: boolean;
avatar?: string;
secrets?: ApiSecret[];
password?: string;
@@ -382,7 +415,6 @@ export enum Operation {
UpdateBuild = "update_build",
DeleteBuild = "delete_build",
BuildBuild = "build_build",
RecloneBuild = "reclone_build",
CreateDeployment = "create_deployment",
UpdateDeployment = "update_deployment",
DeleteDeployment = "delete_deployment",
@@ -400,6 +432,7 @@ export enum Operation {
DeleteGroup = "delete_group",
ModifyUserEnabled = "modify_user_enabled",
ModifyUserCreateServerPermissions = "modify_user_create_server_permissions",
ModifyUserCreateBuildPermissions = "modify_user_create_build_permissions",
ModifyUserPermissions = "modify_user_permissions",
AutoBuild = "auto_build",
AutoPull = "auto_pull",
@@ -449,7 +482,6 @@ export enum ProcedureOperation {
PruneContainersServer = "prune_containers_server",
PruneNetworksServer = "prune_networks_server",
BuildBuild = "build_build",
RecloneBuild = "reclone_build",
DeployContainer = "deploy_container",
StopContainer = "stop_container",
StartContainer = "start_container",

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import fileDownload from "js-file-download";
import { URL } from "..";
import {
AwsBuilderConfig,
BasicContainerInfo,
Build,
BuildActionState,
@@ -38,6 +39,7 @@ import {
CreateSecretBody,
CreateServerBody,
LoginOptions,
ModifyUserCreateBuildBody,
ModifyUserCreateServerBody,
ModifyUserEnabledBody,
PermissionsUpdateBody,
@@ -145,6 +147,10 @@ export class Client {
return this.get(`/api/deployment/${id}/stats`);
}
get_deployment_deployed_version(id: string): Promise<string> {
return this.get(`/api/deployment/${id}/deployed_version`);
}
create_deployment(body: CreateDeploymentBody): Promise<Deployment> {
return this.post("/api/deployment/create", body);
}
@@ -351,6 +357,10 @@ export class Client {
return this.post(`/api/build/${id}/reclone`);
}
get_aws_builder_defaults(): Promise<AwsBuilderConfig> {
return this.get("/api/build/aws_builder_defaults");
}
// procedure
list_procedures(query?: QueryObject): Promise<Procedure[]> {
@@ -454,6 +464,12 @@ export class Client {
return this.post("/api/permissions/modify_create_server", body);
}
modify_user_create_build_permissions(
body: ModifyUserCreateBuildBody
): Promise<Update> {
return this.post("/api/permissions/modify_create_build", body);
}
async get<R = any>(url: string): Promise<R> {
return await axios({
method: "get",

View File

@@ -6,12 +6,10 @@ import { PermissionLevel, PermissionsTarget } from "../types";
export interface CreateBuildBody {
name: string;
server_id: string;
}
export interface CopyBuildBody {
name: string;
server_id: string;
}
export interface BuildVersionsQuery {
@@ -56,6 +54,11 @@ export interface ModifyUserCreateServerBody {
create_server_permissions: boolean;
}
export interface ModifyUserCreateBuildBody {
user_id: string;
create_build_permissions: boolean;
}
export interface CreateProcedureBody {
name: string;
}

View File

@@ -1,12 +1,11 @@
[package]
name = "db_client"
version = "0.1.10"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
types = { package = "monitor_types", version = "0.1.10" }
# types = { package = "monitor_types", path = "../types" }
types = { package = "monitor_types", path = "../types" }
mungos = "0.3.0"
anyhow = "1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_helpers"
version = "0.1.10"
version = "0.2.0"
edition = "2021"
authors = ["MoghTech"]
description = "helpers used as dependency for mogh tech monitor"
@@ -9,8 +9,9 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# types = { package = "monitor_types", path = "../types" }
types = { package = "monitor_types", version = "0.1.10" }
tokio = "1.25"
types = { package = "monitor_types", path = "../types" }
periphery_client = { path = "../periphery_client" }
async_timing_util = "0.1.14"
bollard = "0.13"
anyhow = "1.0"
@@ -18,8 +19,10 @@ axum = { version = "0.6", features = ["ws", "json"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
toml = "0.6"
toml = "0.7"
run_command = { version = "0.0.5", features = ["async_tokio"] }
rand = "0.8"
futures = "0.3"
futures-util = "0.3.25"
aws-config = "0.54"
aws-sdk-ec2 = "0.24"

199
lib/helpers/src/aws/mod.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use aws_sdk_ec2::model::{
BlockDeviceMapping, EbsBlockDevice, InstanceNetworkInterfaceSpecification, InstanceStateChange,
InstanceStateName, InstanceStatus, ResourceType, Tag, TagSpecification,
};
pub use aws_sdk_ec2::{
model::InstanceType,
output::{DescribeInstanceStatusOutput, TerminateInstancesOutput},
Client, Region,
};
use types::Server;
pub async fn create_ec2_client(
region: String,
access_key_id: &str,
secret_access_key: String,
) -> Client {
// There may be a better way to pass these keys to client
std::env::set_var("AWS_ACCESS_KEY_ID", access_key_id);
std::env::set_var("AWS_SECRET_ACCESS_KEY", secret_access_key);
let region = Region::new(region);
let config = aws_config::from_env().region(region).load().await;
let client = Client::new(&config);
client
}
pub struct Ec2Instance {
pub instance_id: String,
pub server: Server,
}
const POLL_RATE_SECS: u64 = 2;
const MAX_POLL_TRIES: usize = 30;
/// this will only resolve after the instance is running
/// should still poll the periphery agent after creation
pub async fn create_instance_with_ami(
client: &Client,
instance_name: &str,
ami_id: &str,
instance_type: &str,
subnet_id: &str,
security_group_ids: Vec<String>,
volume_size_gb: i32,
key_pair_name: &str,
assign_public_ip: bool,
) -> anyhow::Result<Ec2Instance> {
let instance_type = InstanceType::from(instance_type);
if let InstanceType::Unknown(t) = instance_type {
return Err(anyhow!("unknown instance type {t:?}"));
}
let res = client
.run_instances()
.image_id(ami_id)
.instance_type(instance_type)
.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name(String::from("/dev/sda1").into())
.set_ebs(
EbsBlockDevice::builder()
.volume_size(volume_size_gb)
.build()
.into(),
)
.build(),
)
.network_interfaces(
InstanceNetworkInterfaceSpecification::builder()
.subnet_id(subnet_id)
.associate_public_ip_address(assign_public_ip)
.set_groups(security_group_ids.into())
.device_index(0)
.build(),
)
.key_name(key_pair_name)
.tag_specifications(
TagSpecification::builder()
.tags(Tag::builder().key("Name").value(instance_name).build())
.resource_type(ResourceType::Instance)
.build(),
)
.min_count(1)
.max_count(1)
.send()
.await
.context("failed to start builder ec2 instance")?;
let instance = res
.instances()
.ok_or(anyhow!("got None for created instances"))?
.get(0)
.ok_or(anyhow!("instances array is empty"))?;
let instance_id = instance
.instance_id()
.ok_or(anyhow!("instance does not have instance_id"))?
.to_string();
for _ in 0..MAX_POLL_TRIES {
let state_name = get_ec2_instance_state_name(&client, &instance_id).await?;
if state_name == Some(InstanceStateName::Running) {
let ip = if assign_public_ip {
get_ec2_instance_public_ip(client, &instance_id).await?
} else {
instance
.private_ip_address()
.ok_or(anyhow!("instance does not have private ip"))?
.to_string()
};
let server = Server {
address: format!("http://{ip}:8000"),
..Default::default()
};
return Ok(Ec2Instance {
instance_id,
server,
});
}
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
}
Err(anyhow!("instance not running after polling"))
}
pub async fn get_ec2_instance_status(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStatus>> {
let status = client
.describe_instance_status()
.instance_ids(instance_id)
.send()
.await
.context("failed to get instance status from aws")?
.instance_statuses()
.ok_or(anyhow!("instance statuses is None"))?
.get(0)
.map(|s| s.to_owned());
Ok(status)
}
pub async fn get_ec2_instance_state_name(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStateName>> {
let status = get_ec2_instance_status(client, instance_id).await?;
if status.is_none() {
return Ok(None);
}
let state = status
.unwrap()
.instance_state()
.ok_or(anyhow!("instance state is None"))?
.name()
.ok_or(anyhow!("instance state name is None"))?
.to_owned();
Ok(Some(state))
}
pub async fn get_ec2_instance_public_ip(
client: &Client,
instance_id: &str,
) -> anyhow::Result<String> {
let ip = client
.describe_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to get instance status from aws")?
.reservations()
.ok_or(anyhow!("instance reservations is None"))?
.get(0)
.ok_or(anyhow!("instance reservations is empty"))?
.instances()
.ok_or(anyhow!("instances is None"))?
.get(0)
.ok_or(anyhow!("instances is empty"))?
.public_ip_address()
.ok_or(anyhow!("instance has no public ip"))?
.to_string();
Ok(ip)
}
pub async fn terminate_ec2_instance(
client: &Client,
instance_id: &str,
) -> anyhow::Result<InstanceStateChange> {
let res = client
.terminate_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to terminate instance from aws")?
.terminating_instances()
.ok_or(anyhow!("terminating instances is None"))?
.get(0)
.ok_or(anyhow!("terminating instances is empty"))?
.to_owned();
Ok(res)
}

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{anyhow, Context};
use types::{Build, DockerBuildArgs, EnvironmentVar, Log, Version};
use crate::{all_logs_success, git, run_monitor_command, to_monitor_name};
use crate::{run_monitor_command, to_monitor_name};
use super::docker_login;
@@ -17,9 +17,7 @@ pub async fn build(
name,
version,
docker_build_args,
branch,
docker_account,
pre_build,
..
}: &Build,
mut repo_dir: PathBuf,
@@ -38,25 +36,25 @@ pub async fn build(
.await
.context("failed to login to docker")?;
repo_dir.push(&name);
let pull_logs = git::pull(repo_dir.clone(), branch, &None).await;
if !all_logs_success(&pull_logs) {
logs.extend(pull_logs);
return Ok(logs);
}
logs.extend(pull_logs);
if let Some(command) = pre_build {
let dir = repo_dir.join(&command.path);
let pre_build_log = run_monitor_command(
"pre build",
format!("cd {} && {}", dir.display(), command.command),
)
.await;
if !pre_build_log.success {
logs.push(pre_build_log);
return Ok(logs);
}
logs.push(pre_build_log);
}
// let pull_logs = git::pull(repo_dir.clone(), branch, &None).await;
// if !all_logs_success(&pull_logs) {
// logs.extend(pull_logs);
// return Ok(logs);
// }
// logs.extend(pull_logs);
// if let Some(command) = pre_build {
// let dir = repo_dir.join(&command.path);
// let pre_build_log = run_monitor_command(
// "pre build",
// format!("cd {} && {}", dir.display(), command.command),
// )
// .await;
// if !pre_build_log.success {
// logs.push(pre_build_log);
// return Ok(logs);
// }
// logs.push(pre_build_log);
// }
let build_dir = repo_dir.join(build_path);
let dockerfile_path = match dockerfile_path {
Some(dockerfile_path) => dockerfile_path.to_owned(),

View File

@@ -2,47 +2,10 @@ use std::path::PathBuf;
use ::run_command::async_run_command;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use types::{monitor_timestamp, Build, Command, Deployment, GithubToken, GithubUsername, Log};
use types::{monitor_timestamp, CloneArgs, Command, GithubToken, Log};
use crate::{run_monitor_command, to_monitor_name};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CloneArgs {
name: String,
repo: Option<String>,
branch: Option<String>,
on_clone: Option<Command>,
on_pull: Option<Command>,
pub github_account: Option<GithubUsername>,
}
impl From<&Deployment> for CloneArgs {
fn from(d: &Deployment) -> Self {
CloneArgs {
name: d.name.clone(),
repo: d.repo.clone(),
branch: d.branch.clone(),
on_clone: d.on_clone.clone(),
on_pull: d.on_pull.clone(),
github_account: d.github_account.clone(),
}
}
}
impl From<&Build> for CloneArgs {
fn from(b: &Build) -> Self {
CloneArgs {
name: b.name.clone(),
repo: b.repo.clone(),
branch: b.branch.clone(),
on_clone: b.on_clone.clone(),
on_pull: None,
github_account: b.github_account.clone(),
}
}
}
pub async fn pull(
mut path: PathBuf,
branch: &Option<String>,
@@ -120,7 +83,7 @@ async fn clone(
access_token: Option<GithubToken>,
) -> Log {
let _ = std::fs::remove_dir_all(destination);
let access_token = match access_token {
let access_token_at = match &access_token {
Some(token) => format!("{token}@"),
None => String::new(),
};
@@ -128,12 +91,12 @@ async fn clone(
Some(branch) => format!(" -b {branch}"),
None => String::new(),
};
let repo_url = format!("https://{access_token}github.com/{repo}.git");
let repo_url = format!("https://{access_token_at}github.com/{repo}.git");
let command = format!("git clone {repo_url} {destination}{branch}");
let start_ts = monitor_timestamp();
let output = async_run_command(&command).await;
let command = if access_token.len() > 0 {
command.replace(&access_token, "<TOKEN>")
let command = if access_token_at.len() > 0 {
command.replace(&access_token.unwrap(), "<TOKEN>")
} else {
command
};

View File

@@ -7,6 +7,7 @@ use run_command::{async_run_command, CommandOutput};
use serde::de::DeserializeOwned;
use types::{monitor_timestamp, Log};
pub mod aws;
pub mod docker;
pub mod git;

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_client"
version = "0.1.10"
version = "0.2.0"
edition = "2021"
authors = ["MoghTech"]
description = "a client to interact with the monitor system"
@@ -9,14 +9,15 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monitor_types = "0.1.10"
monitor_types = "0.2.0"
# monitor_types = { path = "../types" }
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = { version = "1.24", features = ["full"] }
tokio = { version = "1.25", features = ["full"] }
tokio-util = "0.7"
anyhow = "1.0"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
futures-util = "0.3"
futures-util = "0.3"
envy = "0.4"

View File

@@ -1,5 +1,5 @@
use anyhow::Context;
use monitor_types::{Build, BuildActionState, BuildVersionsReponse, Update};
use monitor_types::{AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse, Update};
use serde_json::{json, Value};
use crate::MonitorClient;
@@ -14,6 +14,7 @@ impl MonitorClient {
pub async fn get_build(&self, build_id: &str) -> anyhow::Result<Build> {
self.get(&format!("/api/build/{build_id}"), Option::<()>::None)
.await
.context(format!("failed at getting build {build_id}"))
}
pub async fn get_build_action_state(&self, build_id: &str) -> anyhow::Result<BuildActionState> {
@@ -22,6 +23,9 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context(format!(
"failed at getting action state for build {build_id}"
))
}
pub async fn get_build_versions(
@@ -37,6 +41,7 @@ impl MonitorClient {
json!({ "page": page, "major": major.into(), "minor": minor.into(), "patch": patch.into() }),
)
.await
.context("failed at getting build versions")
}
pub async fn create_build(&self, name: &str, server_id: &str) -> anyhow::Result<Build> {
@@ -53,18 +58,16 @@ impl MonitorClient {
pub async fn create_full_build(&self, build: &Build) -> anyhow::Result<Build> {
self.post::<&Build, _>("/api/build/create_full", build)
.await
.context(format!("failed at creating full build"))
.context(format!(
"failed at creating full build with name {}",
build.name
))
}
pub async fn copy_build(
&self,
id: &str,
new_name: &str,
new_server_id: &str,
) -> anyhow::Result<Build> {
pub async fn copy_build(&self, id: &str, new_name: &str) -> anyhow::Result<Build> {
self.post(
&format!("/api/build/{id}/copy"),
json!({ "name": new_name, "server_id": new_server_id }),
json!({ "name": new_name }),
)
.await
.context(format!("failed at copying build {id}"))
@@ -88,9 +91,15 @@ impl MonitorClient {
.context(format!("failed at building build {build_id}"))
}
pub async fn reclone_build(&self, id: &str) -> anyhow::Result<Update> {
self.post::<(), _>(&format!("/api/build/{id}/reclone"), None)
pub async fn get_aws_builder_defaults(&self) -> anyhow::Result<AwsBuilderConfig> {
self.get("/api/build/aws_builder_defaults", Option::<()>::None)
.await
.context(format!("failed at recloning build {id}"))
.context("failed at getting aws builder defaults")
}
// pub async fn reclone_build(&self, id: &str) -> anyhow::Result<Update> {
// self.post::<(), _>(&format!("/api/build/{id}/reclone"), None)
// .await
// .context(format!("failed at recloning build {id}"))
// }
}

View File

@@ -35,6 +35,7 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context("failed at get_deployment_action_state")
}
pub async fn get_deployment_container_log(
@@ -56,7 +57,21 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context("failed at get_deployment_container_log")
.context("failed at get_deployment_container_stats")
}
pub async fn get_deployment_deployed_version(
&self,
deployment_id: &str,
) -> anyhow::Result<String> {
self.get(
&format!("/api/deployment/{deployment_id}/deployed_version"),
Option::<()>::None,
)
.await
.context(format!(
"failed at get_deployment_deployed_version for id {deployment_id}"
))
}
pub async fn create_deployment(

View File

@@ -3,7 +3,7 @@
use anyhow::{anyhow, Context};
use monitor_types::User;
use reqwest::StatusCode;
use serde::{de::DeserializeOwned, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::json;
pub use futures_util;
@@ -20,6 +20,15 @@ mod secret;
mod server;
mod update;
#[derive(Deserialize)]
struct MonitorEnv {
monitor_url: String,
monitor_token: Option<String>,
monitor_username: Option<String>,
monitor_password: Option<String>,
monitor_secret: Option<String>,
}
#[derive(Clone)]
pub struct MonitorClient {
http_client: reqwest::Client,
@@ -80,6 +89,26 @@ impl MonitorClient {
Ok(client)
}
pub async fn new_from_env() -> anyhow::Result<MonitorClient> {
let env = envy::from_env::<MonitorEnv>()
.context("failed to parse environment for monitor client")?;
if let Some(token) = env.monitor_token {
Ok(MonitorClient::new_with_token(&env.monitor_url, token))
} else if let Some(password) = env.monitor_password {
let username = env.monitor_username.ok_or(anyhow!(
"must provide MONITOR_USERNAME to authenticate with MONITOR_PASSWORD"
))?;
MonitorClient::new_with_password(&env.monitor_url, username, password).await
} else if let Some(secret) = env.monitor_secret {
let username = env.monitor_username.ok_or(anyhow!(
"must provide MONITOR_USERNAME to authenticate with MONITOR_SECRET"
))?;
MonitorClient::new_with_secret(&env.monitor_url, username, secret).await
} else {
Err(anyhow!("failed to initialize monitor client from env | must provide one of: (MONITOR_TOKEN), (MONITOR_USERNAME and MONITOR_PASSWORD), (MONITOR_USERNAME and MONITOR_SECRET)"))
}
}
pub async fn create_user(
&self,
username: impl Into<String>,

View File

@@ -1,17 +1,14 @@
[package]
name = "periphery_client"
version = "0.1.10"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helpers = { package = "monitor_helpers", version = "0.1.10" }
types = { package = "monitor_types", version = "0.1.10" }
# types = { package = "monitor_types", path = "../types" }
# helpers = { package = "monitor_helpers", path = "../helpers" }
types = { package = "monitor_types", path = "../types" }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = "1.24"
tokio = "1.25"
reqwest = { version = "0.11", features = ["json"] }
serde = "1.0"
serde_json = "1.0"

View File

@@ -1,7 +1,6 @@
use anyhow::Context;
use helpers::git::CloneArgs;
use serde_json::json;
use types::{Command, Log, Server};
use types::{CloneArgs, Command, Log, Server};
use crate::PeripheryClient;

Some files were not shown because too many files have changed in this diff Show More