diff --git a/.dockerignore b/.dockerignore index c269c7eaf..37c369561 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,8 @@ /target -/config_example -config.* -.env \ No newline at end of file +readme.md +typeshare.toml +LICENSE +*.code-workspace + +*/node_modules +*/dist \ No newline at end of file diff --git a/.gitignore b/.gitignore index 48190eb74..6421117ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,9 @@ -target -/frontend/build -node_modules -dist -.env -.env.development - -repos -config.json -config.toml -secrets.json -secrets.toml \ No newline at end of file +target +/frontend/build +node_modules +/lib/ts_client/build +dist +.env +.env.development +creds.toml +core.config.toml \ No newline at end of file diff --git a/.vscode/resolver.code-snippets b/.vscode/resolver.code-snippets new file mode 100644 index 000000000..724485fbe --- /dev/null +++ b/.vscode/resolver.code-snippets @@ -0,0 +1,26 @@ +{ + "resolve": { + "scope": "rust", + "prefix": "resolve", + "body": [ + "#[async_trait]", + "impl Resolve<${1}, User> for State {", + "\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {", + "\t\ttodo!()", + "\t}", + "}" + ] + }, + "static": { + "scope": "rust", + "prefix": "static", + "body": [ + "fn ${1}() -> &'static ${2} {", + "\tstatic ${3}: OnceLock<${2}> = OnceLock::new();", + "\t${3}.get_or_init(|| {", + "\t\t${0}", + "\t})", + "}" + ] + } +} \ No newline at end of file diff --git a/.vscode/solid.code-snippets b/.vscode/solid.code-snippets deleted file mode 100644 index 2ded946e2..000000000 --- a/.vscode/solid.code-snippets +++ /dev/null @@ -1,64 +0,0 @@ -{ - "component": { - "scope": "typescriptreact,javascriptreact", - "prefix": "comp", - "body": [ - "import { Component } from \"solid-js\";", - "", - "const ${1:$TM_FILENAME_BASE}: Component<{}> = (p) => {", - "\treturn (", - "\t\t
", - "\t\t\t${0}", - "\t\t
", - "\t);", - "}", - "", - "export default ${1:$TM_FILENAME_BASE};" - ] - }, - "component-with-css": { - "scope": "typescriptreact,javascriptreact", - "prefix": "css-comp", - "body": [ - "import { Component } from \"solid-js\";", - "import s from \"./${1:$TM_FILENAME_BASE}.module.scss\";", - "", - "const ${2:$TM_FILENAME_BASE}: Component<{}> = (p) => {", - "\treturn (", - "\t\t
", - "\t\t\t${0}", - "\t\t
", - "\t);", - "}", - "", - "export default ${2:$TM_FILENAME_BASE};" - ] - }, - "context": { - "scope": "typescriptreact,javascriptreact", - "prefix": "provider", - "body": [ - "import { ParentComponent, createContext, useContext } from \"solid-js\";", - "", - "const value = () => {", - "\treturn {};", - "}", - "", - "export type Value = ReturnType;", - "", - "const context = createContext();", - "", - "export const Provider: ParentComponent<{}> = (p) => {", - "\treturn (", - "\t\t", - "\t\t\t{p.children}", - "\t\t", - "\t);", - "}", - "", - "export function useValue() {", - "\treturn useContext(context) as Value;", - "}" - ] - } -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 26ac37fc9..758c060b3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -24,14 +24,14 @@ "label": "start dev", "dependsOn": [ "run core", - "yarn: start frontend" + "start frontend" ], "problemMatcher": [] }, { "type": "shell", "command": "yarn start", - "label": "yarn: start frontend", + "label": "start frontend", "options": { "cwd": "${workspaceFolder}/frontend" }, @@ -44,7 +44,7 @@ "command": "run", "label": "run core", "options": { - "cwd": "${workspaceFolder}/core" + "cwd": "${workspaceFolder}/bin/core" }, "presentation": { "group": "start" @@ -55,24 +55,7 @@ "command": "run", "label": "run periphery", "options": { - "cwd": "${workspaceFolder}/periphery" - } - }, - { - "type": "shell", - "command": "cargo install --path . && if pgrep periphery; then pkill periphery; fi && periphery --daemon --config-path ~/.monitor/local.periphery.config.toml", - "label": "run periphery daemon", - "options": { - "cwd": "${workspaceFolder}/periphery" - }, - "problemMatcher": [] - }, - { - "type": "cargo", - "command": "run", - "label": "run cli", - "options": { - "cwd": "${workspaceFolder}/cli" + "cwd": "${workspaceFolder}/bin/periphery" } }, { @@ -80,14 +63,14 @@ "command": "run", "label": "run tests", "options": { - "cwd": "${workspaceFolder}/tests" + "cwd": "${workspaceFolder}/bin/tests" } }, { "type": "cargo", "command": "publish", "args": ["--allow-dirty"], - "label": "publish monitor types", + "label": "publish types", "options": { "cwd": "${workspaceFolder}/lib/types" } @@ -95,68 +78,14 @@ { "type": "cargo", "command": "publish", - "label": "publish monitor client", + "label": "publish rs client", "options": { - "cwd": "${workspaceFolder}/lib/monitor_client" + "cwd": "${workspaceFolder}/lib/rs_client" } }, { "type": "shell", - "command": "docker compose up -d", - "label": "docker compose up", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "docker compose down", - "label": "docker compose down", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "docker compose build", - "label": "docker compose build", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "docker compose down && docker compose up -d", - "label": "docker compose restart", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "docker compose build && docker compose down && docker compose up -d", - "label": "docker compose build and restart", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "docker compose build periphery", - "label": "docker compose build periphery", - "options": { - "cwd": "${workspaceFolder}/tests" - }, - "problemMatcher": [] - }, - { - "type": "shell", - "command": "typeshare ./lib/types --lang=typescript --output-file=./frontend/src/types.ts && typeshare ./core --lang=typescript --output-file=./frontend/src/util/client_types.ts", + "command": "node ./client/ts/generate_types.mjs", "label": "generate typescript types", "problemMatcher": [] } diff --git a/Cargo.lock b/Cargo.lock index 7620883fa..dddd154ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,24 +19,39 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ + "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +name = "alert_logger" +version = "1.1.0" dependencies = [ - "memchr", + "anyhow", + "axum 0.7.5", + "dotenv", + "envy", + "logger", + "monitor_client", + "serde", + "termination_signal", + "tokio", + "tracing", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -39,59 +63,100 @@ dependencies = [ [[package]] name = "anstream" -version = "0.2.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" dependencies = [ "anstyle", "anstyle-parse", + "anstyle-query", "anstyle-wincon", - "concolor-override", - "concolor-query", - "is-terminal", + "colorchoice", "utf8parse", ] [[package]] name = "anstyle" -version = "0.3.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[package]] name = "anstyle-parse" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" dependencies = [ "utf8parse", ] [[package]] -name = "anstyle-wincon" -version = "0.2.0" +name = "anstyle-query" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "anyhow" -version = "1.0.69" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] -name = "async-trait" -version = "0.1.64" +name = "async-recursion" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", ] [[package]] @@ -102,22 +167,11 @@ checksum = "9de37a9f9ebc2a377f452555a2278e89548c7746c9a47cf3047b30366af7168e" dependencies = [ "serde", "serde_derive", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "tokio", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -126,339 +180,372 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "aws-config" -version = "0.55.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd" +checksum = "e2a89e0000cde82447155d64eeb71720b933b4396a6fbbebad3f8b4f88ca7b54" dependencies = [ "aws-credential-types", - "aws-http", + "aws-runtime", "aws-sdk-sso", + "aws-sdk-ssooidc", "aws-sdk-sts", "aws-smithy-async", - "aws-smithy-client", "aws-smithy-http", - "aws-smithy-http-tower", "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "fastrand", + "fastrand 2.0.0", "hex", - "http", - "hyper", - "ring", - "time 0.3.20", + "http 0.2.9", + "hyper 0.14.26", + "ring 0.17.7", + "time 0.3.21", "tokio", - "tower", "tracing", + "url", "zeroize", ] [[package]] name = "aws-credential-types" -version = "0.55.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api", "aws-smithy-types", - "fastrand", - "tokio", - "tracing", "zeroize", ] [[package]] -name = "aws-endpoint" -version = "0.55.2" +name = "aws-runtime" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "aws-types", - "http", - "regex", - "tracing", -] - -[[package]] -name = "aws-http" -version = "0.55.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671" +checksum = "f4963ac9ff2d33a4231b3806c1c69f578f221a9cabb89ad2bde62ce2b442c8a7" dependencies = [ "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", "aws-smithy-http", + "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "http", - "http-body", - "lazy_static", + "fastrand 2.0.0", + "http 0.2.9", + "http-body 0.4.5", "percent-encoding", "pin-project-lite", "tracing", + "uuid", ] [[package]] name = "aws-sdk-ec2" -version = "0.27.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab77050ecd90bf116dcbff6a7b8b2fb294b1399900a97e870c4879fe2010827" +checksum = "4b58453a3033a5c2e1631abec61b6915c507a726d628a0c2d7b81578d81be1b3" dependencies = [ "aws-credential-types", - "aws-endpoint", - "aws-http", - "aws-sig-auth", + "aws-runtime", "aws-smithy-async", - "aws-smithy-client", "aws-smithy-http", - "aws-smithy-http-tower", "aws-smithy-json", "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", - "fastrand", - "http", - "regex", - "tokio-stream", - "tower", + "fastrand 2.0.0", + "http 0.2.9", + "once_cell", + "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "0.27.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6" +checksum = "32fcc572fd5c58489ec205ec3e4e5f7d63018898a485cbf922a462af496bc300" dependencies = [ "aws-credential-types", - "aws-endpoint", - "aws-http", - "aws-sig-auth", + "aws-runtime", "aws-smithy-async", - "aws-smithy-client", "aws-smithy-http", - "aws-smithy-http-tower", "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "http", - "regex", - "tokio-stream", - "tower", + "http 0.2.9", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b6275fa8684a1192754221173b1f7a7c1260d6b0571cc2b8af09468eb0cffe5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.9", + "once_cell", + "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "0.27.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1" +checksum = "30acd58272fd567e4853c5075d838be1626b59057e0249c9be5a1a7eb13bf70f" dependencies = [ "aws-credential-types", - "aws-endpoint", - "aws-http", - "aws-sig-auth", + "aws-runtime", "aws-smithy-async", - "aws-smithy-client", "aws-smithy-http", - "aws-smithy-http-tower", "aws-smithy-json", "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", - "http", - "regex", - "tower", - "tracing", -] - -[[package]] -name = "aws-sig-auth" -version = "0.55.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-http", - "aws-types", - "http", + "http 0.2.9", + "once_cell", + "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "0.55.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e" +checksum = "11d6f29688a4be9895c0ba8bef861ad0c0dac5c15e9618b9b7a6c233990fc263" dependencies = [ + "aws-credential-types", "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", "form_urlencoded", "hex", "hmac", - "http", + "http 0.2.9", + "http 1.0.0", "once_cell", "percent-encoding", - "regex", "sha2", - "time 0.3.20", + "time 0.3.21", "tracing", ] [[package]] name = "aws-smithy-async" -version = "0.55.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" dependencies = [ "futures-util", "pin-project-lite", "tokio", - "tokio-stream", -] - -[[package]] -name = "aws-smithy-client" -version = "0.55.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba" -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", - "rustls", - "tokio", - "tower", - "tracing", ] [[package]] name = "aws-smithy-http" -version = "0.55.2" +version = "0.60.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5" +checksum = "3f10fa66956f01540051b0aa7ad54574640f748f9839e843442d99b970d3aff9" dependencies = [ + "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "bytes-utils", "futures-core", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "aws-smithy-http-tower" -version = "0.55.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "http", - "http-body", - "pin-project-lite", - "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.55.2" +version = "0.60.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-query" -version = "0.55.2" +version = "0.60.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" dependencies = [ "aws-smithy-types", "urlencoding", ] [[package]] -name = "aws-smithy-types" -version = "0.55.2" +name = "aws-smithy-runtime" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa" +checksum = "de34bcfa1fb3c82a80e252a753db34a6658e07f23d3a5b3fc96919518fa7a3f5" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand 2.0.0", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", + "http-body 1.0.0", + "hyper 0.14.26", + "hyper-rustls", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc56a5c96ec741de6c5e6bf1ce6948be969d6506dfa9c39cffc284e31e4979b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.9", + "http 1.0.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe14dceea1e70101d38fbf2a99e6a34159477c0fb95e68e05c66bd7ae4c3729" dependencies = [ "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.9", + "http 1.0.0", + "http-body 0.4.5", + "http-body 1.0.0", + "http-body-util", "itoa", "num-integer", + "pin-project-lite", + "pin-utils", "ryu", - "time 0.3.20", + "serde", + "time 0.3.21", + "tokio", + "tokio-util", ] [[package]] name = "aws-smithy-xml" -version = "0.55.2" +version = "0.60.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c" +checksum = "872c68cf019c0e4afc5de7753c4f7288ce4b71663212771bf5e4542eb9346ca9" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "0.55.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b" +checksum = "5a43b56df2c529fe44cb4d92bd64d0479883fb9608ff62daede4df5405381814" dependencies = [ "aws-credential-types", "aws-smithy-async", - "aws-smithy-client", - "aws-smithy-http", + "aws-smithy-runtime-api", "aws-smithy-types", - "http", + "http 0.2.9", "rustc_version 0.4.0", "tracing", ] [[package]] name = "axum" -version = "0.6.9" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6137c6234afb339e75e764c866e3594900f0211e1315d33779f269bbe2ec6967" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", - "base64 0.21.0", - "bitflags", + "axum-core 0.3.4", + "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.26", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 0.1.2", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "axum-macros", + "base64 0.21.5", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", "itoa", "matchit", "memchr", @@ -471,26 +558,26 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite 0.18.0", + "tokio-tungstenite", "tower", - "tower-http", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", @@ -498,18 +585,74 @@ dependencies = [ ] [[package]] -name = "axum_oauth2" -version = "0.1.0" +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ - "anyhow", - "axum", - "jwt", - "rand", - "reqwest", + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum 0.7.5", + "axum-core 0.4.3", + "bytes", + "futures-util", + "headers", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", "serde", - "serde_derive", - "serde_json", - "urlencoding", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", ] [[package]] @@ -520,9 +663,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "base64-simd" @@ -536,11 +685,11 @@ dependencies = [ [[package]] name = "bcrypt" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" dependencies = [ - "base64 0.21.0", + "base64 0.22.0", "blowfish", "getrandom", "subtle", @@ -553,6 +702,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "bitvec" version = "1.0.1" @@ -567,9 +722,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -586,19 +741,22 @@ dependencies = [ [[package]] name = "bollard" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af254ed2da4936ef73309e9597180558821cb16ae9bba4cb24ce6b612d8d80ed" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" dependencies = [ - "base64 0.21.0", + "base64 0.22.0", "bollard-stubs", "bytes", "futures-core", "futures-util", "hex", - "http", - "hyper", - "hyperlocal", + "http 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-named-pipe", + "hyper-util", + "hyperlocal-next", "log", "pin-project-lite", "serde", @@ -609,46 +767,48 @@ dependencies = [ "thiserror", "tokio", "tokio-util", + "tower-service", "url", "winapi", ] [[package]] name = "bollard-stubs" -version = "1.42.0-rc.7" +version = "1.44.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602bda35f33aeb571cef387dcd4042c643a8bf689d8aaac2cc47ea24cb7bc7e0" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" dependencies = [ "serde", - "serde_with 2.2.0", + "serde_repr", + "serde_with 3.3.0", ] [[package]] name = "bson" -version = "2.6.1" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aeb8bae494e49dbc330dd23cf78f6f7accee22f640ce3ab17841badaa4ce232" +checksum = "88c18b51216e1f74b9d769cead6ace2f82b965b807e3d73330aabe9faec31c84" dependencies = [ "ahash", "base64 0.13.1", "bitvec", "hex", - "indexmap", + "indexmap 1.9.3", "js-sys", - "lazy_static", + "once_cell", "rand", "serde", "serde_bytes", "serde_json", - "time 0.3.20", + "time 0.3.21", "uuid", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "byteorder" @@ -674,11 +834,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -689,13 +850,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", "time 0.1.45", @@ -705,9 +866,9 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", @@ -715,81 +876,49 @@ dependencies = [ [[package]] name = "clap" -version = "4.2.1" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.2.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "bitflags", "clap_lex", - "strsim", + "strsim 0.11.0", ] [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.53", ] [[package]] name = "clap_lex" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "concolor-override" +name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" - -[[package]] -name = "concolor-query" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" -dependencies = [ - "windows-sys 0.45.0", -] +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "convert_case" @@ -797,43 +926,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "core" -version = "0.3.4" -dependencies = [ - "anyhow", - "async_timing_util", - "aws-config", - "aws-sdk-ec2", - "axum", - "axum_oauth2", - "bcrypt", - "db_client", - "diff-struct", - "dotenv", - "envy", - "futures-util", - "hex", - "hmac", - "jwt", - "merge_config_files", - "monitor_helpers", - "monitor_types 0.3.4", - "mungos", - "periphery_client", - "serde", - "serde_json", - "sha2", - "slack_client_rs", - "termination_signal", - "tokio", - "tokio-tungstenite 0.19.0", - "tokio-util", - "tower", - "tower-http", - "typeshare", -] - [[package]] name = "core-foundation" version = "0.9.3" @@ -846,15 +938,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -870,19 +962,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -891,9 +982,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", @@ -904,12 +995,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -921,50 +1009,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cxx" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.109", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling" version = "0.13.4" @@ -977,12 +1021,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ - "darling_core 0.14.3", - "darling_macro 0.14.3", + "darling_core 0.20.8", + "darling_macro 0.20.8", ] [[package]] @@ -995,22 +1039,22 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] [[package]] name = "darling_core" -version = "0.14.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim", - "syn 1.0.109", + "strsim 0.10.0", + "syn 2.0.53", ] [[package]] @@ -1026,29 +1070,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.14.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ - "darling_core 0.14.3", + "darling_core 0.20.8", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" - -[[package]] -name = "db_client" -version = "0.3.4" -dependencies = [ - "anyhow", - "monitor_types 0.3.4", - "mungos", -] +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "derivative" @@ -1063,33 +1098,64 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.14.3", + "darling 0.20.8", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "derive_builder_macro" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 1.0.109", + "syn 2.0.53", +] + +[[package]] +name = "derive_default_builder" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4bbfc9b0318750f07951ba6fdd6c886eaa0c68e1164cd2a7a8a540d21262aab" +dependencies = [ + "derive_default_builder_derive", +] + +[[package]] +name = "derive_default_builder_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e520b61247a9470ec86a98baf2aebae5c6dd0f25f02b1c87cafe45b06c160e7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "derive_empty_traits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c90da6aa09bad94e4411461560183be70bb33bc30efb2ce941492f22ed6850" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", ] [[package]] @@ -1106,32 +1172,21 @@ dependencies = [ ] [[package]] -name = "diff-struct" -version = "0.5.1" +name = "derive_variants" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a699d33d273c6226fd3e8f310a9c90ca94f72be84f7aed95b22bb676f34fc90" -dependencies = [ - "diff_derive", - "num", - "serde", -] - -[[package]] -name = "diff_derive" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe165e7ead196bbbf44c7ce11a7a21157b5c002ce46d7098ff9c556784a4912d" +checksum = "4140db2d06b71f45b6c3ab1a30db4c024a930b5b87c207ff8e8c8bfc2f13e461" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -1165,7 +1220,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -1181,14 +1236,20 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.2.8" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1211,10 +1272,16 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.0.25" +name = "fastrand" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -1243,9 +1310,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1258,9 +1325,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1273,9 +1340,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1283,15 +1350,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1300,38 +1367,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.53", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1347,9 +1414,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1357,9 +1424,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -1367,18 +1434,49 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.3.15" +name = "gimli" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 0.2.9", + "indexmap 2.2.5", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.2.5", "slab", "tokio", "tokio-util", @@ -1392,13 +1490,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "hdrhistogram" -version = "7.5.2" +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "byteorder", - "num-traits", + "base64 0.21.5", + "bytes", + "headers-core", + "http 1.0.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.0.0", ] [[package]] @@ -1408,13 +1526,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1468,6 +1583,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -1475,15 +1601,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "http-range-header" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" [[package]] name = "httparse" @@ -1499,22 +1648,22 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1522,13 +1671,50 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.23.2" +name = "hyper" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ - "http", - "hyper", + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.3", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.2.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http 0.2.9", + "hyper 0.14.26", "log", "rustls", "rustls-native-certs", @@ -1536,6 +1722,18 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.26", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1543,47 +1741,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.26", "native-tls", "tokio", "tokio-native-tls", ] [[package]] -name = "hyperlocal" -version = "0.8.0" +name = "hyper-tls" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "futures-util", - "hex", - "hyper", - "pin-project", + "bytes", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "native-tls", "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows 0.48.0", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1605,9 +1840,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1615,12 +1850,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", "serde", ] @@ -1644,64 +1890,62 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "ipconfig" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.5", "widestring", - "winapi", - "winreg", + "windows-sys 0.48.0", + "winreg 0.50.0", ] [[package]] name = "ipnet" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] -name = "is-terminal" -version = "0.4.4" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.45.0", + "either", ] [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1729,18 +1973,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linked-hash-map" @@ -1750,15 +1985,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1766,11 +2001,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "logger" +version = "1.1.0" dependencies = [ - "cfg-if", + "anyhow", + "monitor_client", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", ] [[package]] @@ -1817,18 +2063,18 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] [[package]] name = "merge_config_files" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ed7c516ae04f732a7651e6bbbbdfd7762170fca6b6d263c14a105c41cab9a5" +checksum = "268a14a3b7df38fd426624ed6398cc3358ceb0717761c308a095545afd9dda80" dependencies = [ "serde", "serde_json", @@ -1836,11 +2082,29 @@ dependencies = [ "toml", ] +[[package]] +name = "migrator" +version = "1.1.0" +dependencies = [ + "anyhow", + "chrono", + "dotenv", + "envy", + "logger", + "mongo_indexed", + "monitor_client", + "mungos", + "serde", + "termination_signal", + "tokio", + "tracing", +] + [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -1854,34 +2118,57 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "mongo_indexed" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0cd571ac04a8ac4751e6899f404e5d1370ec79f17609ea5a3caa823619b7513" +dependencies = [ + "anyhow", + "async-trait", + "mongo_indexed_derive", + "mongodb", + "serde", +] + +[[package]] +name = "mongo_indexed_derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da28743409e01e8a63877563786b227285423a0f36c3c16e83f6b8226de9990e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", ] [[package]] name = "mongodb" -version = "2.5.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe15399de63ad4294c80069967736cbb87ebe467a8cd0629df9cab88a6fbde6" +checksum = "46c30763a5c6c52079602be44fa360ca3bfacee55fca73f4734aecd23706a7f2" dependencies = [ "async-trait", "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bson", "chrono", "derivative", @@ -1900,16 +2187,16 @@ dependencies = [ "rand", "rustc_version_runtime", "rustls", - "rustls-pemfile", + "rustls-pemfile 1.0.2", "serde", "serde_bytes", "serde_with 1.14.0", "sha-1", "sha2", "snap", - "socket2", + "socket2 0.4.9", "stringprep", - "strsim", + "strsim 0.10.0", "take_mut", "thiserror", "tokio", @@ -1923,122 +2210,157 @@ dependencies = [ "zstd", ] -[[package]] -name = "monitor_cli" -version = "0.3.4" -dependencies = [ - "async_timing_util", - "clap", - "colored", - "monitor_types 0.3.4", - "rand", - "run_command 0.0.5", - "serde", - "serde_derive", - "strum", - "strum_macros", - "toml", -] - [[package]] name = "monitor_client" -version = "0.3.4" +version = "1.1.0" dependencies = [ "anyhow", + "async_timing_util", + "bollard", + "clap", + "derive_builder", + "derive_default_builder", + "derive_empty_traits", + "derive_variants", "envy", - "futures-util", - "monitor_types 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest", + "futures", + "mongo_indexed", + "mungos", + "partial_derive2", + "reqwest 0.12.3", + "resolver_api", "serde", - "serde_derive", "serde_json", + "serror", + "strum 0.26.2", + "thiserror", "tokio", - "tokio-tungstenite 0.19.0", + "tokio-tungstenite", "tokio-util", + "tracing", + "typeshare", + "uuid", ] [[package]] -name = "monitor_helpers" -version = "0.3.4" +name = "monitor_core" +version = "1.1.0" dependencies = [ "anyhow", - "axum", - "monitor_types 0.3.4", + "async-recursion", + "async-trait", + "async_timing_util", + "aws-config", + "aws-sdk-ec2", + "axum 0.7.5", + "axum-extra", + "bcrypt", + "dotenv", + "envy", + "futures", + "hex", + "hmac", + "jwt", + "logger", + "merge_config_files", + "mongo_indexed", + "monitor_client", + "mungos", + "parse_csl", + "periphery_client", "rand", + "reqwest 0.12.3", + "resolver_api", + "serde", + "serde_json", + "serror", + "sha2", + "slack_client_rs", + "termination_signal", + "tokio", + "tokio-util", + "toml", + "tower", + "tower-http", + "tracing", + "typeshare", + "urlencoding", + "uuid", ] [[package]] name = "monitor_periphery" -version = "0.3.4" +version = "1.1.0" dependencies = [ "anyhow", + "async-trait", "async_timing_util", - "axum", + "axum 0.7.5", + "axum-extra", "bollard", "clap", "dotenv", "envy", - "futures", + "logger", "merge_config_files", - "monitor_helpers", - "monitor_types 0.3.4", + "monitor_client", "parse_csl", - "run_command 0.0.6", + "periphery_client", + "resolver_api", + "run_command", "serde", "serde_json", + "serror", "svi", "sysinfo", + "termination_signal", "tokio", - "tower", + "tracing", + "uuid", ] [[package]] -name = "monitor_types" -version = "0.3.4" +name = "monitor_sync" +version = "1.1.0" dependencies = [ "anyhow", - "bollard", - "bson", - "chrono", - "derive_builder", - "diff-struct", + "clap", + "dotenv", + "merge_config_files", + "monitor_client", "serde", - "strum", - "strum_macros", - "typeshare", + "tokio", ] [[package]] -name = "monitor_types" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ce7541ee0fad1452ae11d0db73285cfbb9df79ad3e49c0680d0bc27c64db24" +name = "monrun" +version = "1.1.0" dependencies = [ "anyhow", - "bollard", - "bson", - "chrono", - "derive_builder", - "diff-struct", + "clap", + "futures", + "monitor_client", + "partial_derive2", "serde", - "serde_derive", - "strum", - "strum_macros", - "typeshare", + "strum 0.26.2", + "tokio", + "toml", + "tracing", + "tracing-subscriber", ] [[package]] name = "mungos" -version = "0.3.19" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d2b7c11b7b18b1436117633a8ec1d7d037530456c6603d5296499f0e38185" +checksum = "ecb32eadede40c482d0220c7949fcd5f1620185aa6317a1fc19b3b42ea0615b1" dependencies = [ "anyhow", + "async-trait", "envy", "futures", "mongodb", "serde", - "tokio", ] [[package]] @@ -2061,45 +2383,21 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] -name = "num" -version = "0.4.0" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" -dependencies = [ - "num-traits", + "overload", + "winapi", ] [[package]] @@ -2112,29 +2410,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -2155,18 +2430,27 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.17.1" +name = "object" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -2177,13 +2461,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] @@ -2194,23 +2478,111 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", + "urlencoding", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" +dependencies = [ + "async-trait", + "futures-core", + "http 0.2.9", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "prost", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ab5bd6c42fb9349dcf28af2ba9a0667f697f9bdcca045d39f2cec5543e2910" + +[[package]] +name = "opentelemetry_sdk" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + [[package]] name = "outref" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2223,15 +2595,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] [[package]] @@ -2240,6 +2612,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffa94c2e5674923c67d7f3dfce1279507b191e10eb064881b46ed3e1256e5ca6" +[[package]] +name = "partial_derive2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bf4a15b30c642aa810f6a33cc8c0dfddd2fa46918ca068fde958882d6b18b" +dependencies = [ + "partial_derive2_derive", +] + +[[package]] +name = "partial_derive2_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49f7410574cdfb56362c529cb2893a246e147072d5e993c46341f02891f9cd1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -2251,49 +2643,49 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "periphery_client" -version = "0.3.4" +version = "1.1.0" dependencies = [ "anyhow", - "futures-util", - "monitor_types 0.3.4", - "reqwest", + "monitor_client", + "reqwest 0.12.3", + "resolver_api", "serde", "serde_json", - "tokio", - "tokio-tungstenite 0.19.0", + "serror", + "tracing", ] [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -2303,9 +2695,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" @@ -2315,13 +2707,36 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.54" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2330,9 +2745,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.26" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2375,9 +2790,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -2385,58 +2800,45 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] -name = "regex" -version = "1.7.1" +name = "regex-lite" +version = "0.1.5" 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" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64 0.21.0", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.26", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2445,9 +2847,12 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile 1.0.2", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", @@ -2455,7 +2860,49 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +dependencies = [ + "base64 0.22.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.3", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.52.0", ] [[package]] @@ -2468,6 +2915,31 @@ dependencies = [ "quick-error", ] +[[package]] +name = "resolver_api" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43981e6bc9a85f1072ffd38bcaca5823e3cd7f24bb675fa5e79736a318f1f998" +dependencies = [ + "anyhow", + "async-trait", + "resolver_api_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "resolver_api_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0af2b29987c0bff183294b58b5b084f16c8f1b77951c7c72b7c70d66cf91ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "ring" version = "0.16.20" @@ -2477,17 +2949,25 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] -name = "run_command" -version = "0.0.5" +name = "ring" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb00d9c9c67906454ba83e62aaa52368814512ac1c5b65b8791bb313a9257b0e" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] [[package]] name = "run_command" @@ -2498,6 +2978,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.2.3" @@ -2513,7 +2999,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.16", + "semver 1.0.17", ] [[package]] @@ -2528,38 +3014,38 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.8" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", + "ring 0.17.7", + "rustls-webpki", "sct", - "webpki", ] [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.2", "schannel", "security-framework", ] @@ -2570,20 +3056,46 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.5", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" @@ -2600,29 +3112,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" - [[package]] name = "sct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2631,9 +3137,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -2650,9 +3156,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "semver-parser" @@ -2662,9 +3168,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -2680,22 +3186,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ - "indexmap", + "indexmap 2.2.5", "itoa", "ryu", "serde", @@ -2703,29 +3209,29 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" dependencies = [ "serde", ] [[package]] name = "serde_repr" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -2754,17 +3260,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "2.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d904179146de381af4c93d3af6ca4984b3152db687dacb9c3c35e86f39809c" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" dependencies = [ - "base64 0.13.1", + "base64 0.21.5", "chrono", "hex", - "indexmap", + "indexmap 1.9.3", + "indexmap 2.2.5", "serde", "serde_json", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -2779,6 +3286,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "serror" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c430d274ef4964c27e7b338fdfed6d400b18ceac80cba827ddd84055f9eca8" +dependencies = [ + "anyhow", + "axum 0.7.5", + "axum-extra", + "serde", + "serde_json", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -2803,15 +3323,24 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook" version = "0.3.15" @@ -2854,24 +3383,24 @@ dependencies = [ [[package]] name = "slack_client_rs" -version = "0.0.8" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c9597f2b960b592f97701c89c9ceb2658c8f804f39f876e1ab25379080fe2e" +checksum = "4c78a07312af77255ee84e41fe32546f97ffc06c4166e753f005888dd12d967f" dependencies = [ "anyhow", - "reqwest", + "reqwest 0.11.27", "serde", "serde_derive", "serde_json", - "strum", - "strum_macros", + "strum 0.25.0", + "strum_macros 0.25.3", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snap" @@ -2889,12 +3418,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stringprep" version = "0.1.2" @@ -2911,19 +3456,40 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.2", +] + [[package]] name = "strum_macros" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -2931,10 +3497,36 @@ dependencies = [ ] [[package]] -name = "subtle" -version = "2.4.1" +name = "strum_macros" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.53", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.53", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "svi" @@ -2958,9 +3550,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.12" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -2974,10 +3566,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "sysinfo" -version = "0.29.0" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "sysinfo" +version = "0.30.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2985,7 +3583,28 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "winapi", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3002,31 +3621,23 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", - "fastrand", + "fastrand 1.9.0", "redox_syscall", "rustix", - "windows-sys 0.42.0", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", + "windows-sys 0.48.0", ] [[package]] name = "termination_signal" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8af403325efec5c1f2ab0f69732a47680c40c5379b088958d053cf946b6bfa" +checksum = "16e2993a3469a2732b1c07df31a0227f2354500779c175f42af4c21693f1ad8f" dependencies = [ "anyhow", "futures", @@ -3037,37 +3648,50 @@ dependencies = [ [[package]] name = "tests" -version = "0.1.0" +version = "1.1.0" dependencies = [ "anyhow", - "async_timing_util", "dotenv", "envy", + "logger", "monitor_client", + "mungos", + "partial_derive2", + "rand", "serde", - "serde_derive", "serde_json", "tokio", + "tracing", ] [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -3083,9 +3707,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -3095,15 +3719,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -3125,11 +3749,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -3137,20 +3761,30 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] [[package]] -name = "tokio-macros" -version = "2.1.0" +name = "tokio-io-timeout" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.53", ] [[package]] @@ -3165,20 +3799,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -3187,35 +3820,21 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.18.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" -dependencies = [ - "futures-util", - "log", - "native-tls", - "tokio", - "tokio-native-tls", - "tungstenite 0.19.0", + "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3228,9 +3847,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", @@ -3240,26 +3859,53 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", "winnow", ] +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.6.20", + "base64 0.21.5", + "bytes", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.26", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -3268,8 +3914,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "hdrhistogram", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand", @@ -3283,16 +3928,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.4.1", "bytes", - "futures-core", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "http-range-header", "httpdate", "mime", @@ -3301,7 +3946,6 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", "tower-layer", "tower-service", "tracing", @@ -3321,11 +3965,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -3334,22 +3977,79 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -3405,36 +4105,16 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" -dependencies = [ - "base64 0.13.1", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.0.0", "httparse", "log", - "native-tls", "rand", "sha1", "thiserror", @@ -3461,9 +4141,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "typeshare" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44d1a2f454cb35fbe05b218c410792697e76bd868f48d3a418f2cd1a7d527d6" +checksum = "99877a66770b25072a67101e80790fd7fe9ed9eff8c69ffb81e9bf5cbea7733f" dependencies = [ "chrono", "serde", @@ -3473,34 +4153,34 @@ dependencies = [ [[package]] name = "typeshare-annotation" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc670d0e358428857cc3b4bf504c691e572fccaec9542ff09212d3f13d74b7a9" +checksum = "ecce25dea8aeaadc44909f4c1226d22d84512fccd07d22447ecbad176bc09545" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.53", ] [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -3511,12 +4191,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "untrusted" version = "0.7.1" @@ -3524,21 +4198,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] -name = "url" -version = "2.3.1" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "update_logger" +version = "1.1.0" +dependencies = [ + "anyhow", + "logger", + "monitor_client", + "termination_signal", + "tokio", + "tracing", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna 0.4.0", "percent-encoding", ] [[package]] name = "urlencoding" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf-8" @@ -3554,14 +4246,21 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", + "rand", "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3604,9 +4303,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3614,24 +4313,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -3641,9 +4340,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3651,57 +4350,54 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "ring", - "untrusted", + "js-sys", + "wasm-bindgen", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "widestring" -version = "0.5.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -3719,43 +4415,53 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3767,21 +4473,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows-targets" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - [[package]] name = "windows-targets" version = "0.48.0" @@ -3798,10 +4489,25 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.1" +name = "windows-targets" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -3810,10 +4516,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" +name = "windows_aarch64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -3822,10 +4534,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] -name = "windows_i686_gnu" -version = "0.42.1" +name = "windows_aarch64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -3834,10 +4552,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] -name = "windows_i686_msvc" -version = "0.42.1" +name = "windows_i686_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -3846,10 +4570,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" +name = "windows_i686_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -3858,10 +4588,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.1" +name = "windows_x86_64_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -3870,10 +4606,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" +name = "windows_x86_64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -3882,21 +4624,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] -name = "winnow" -version = "0.3.3" +name = "windows_x86_64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -3915,10 +4674,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" [[package]] -name = "zeroize" -version = "1.5.7" +name = "zerocopy" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zstd" @@ -3941,9 +4720,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.7+zstd.1.5.4" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index bd8bc329d..52b4b7819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,94 @@ -[workspace] - -members = [ - "cli", - "core", - "periphery", - "tests", - "lib/axum_oauth2", - "lib/db_client", - "lib/helpers", - "lib/periphery_client", - "lib/types", - "lib/monitor_client" -] \ No newline at end of file +[workspace] +resolver = "2" +members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"] + +[workspace.package] +version = "1.1.0" +edition = "2021" +authors = ["mbecker20 "] +license = "GPL-3.0-or-later" + +[workspace.dependencies] +# LOCAL +monitor_macros = { path = "lib/macros" } +monitor_client = { path = "client/core/rs" } +periphery_client = { path = "client/periphery/rs" } +logger = { path = "lib/logger" } + +# MOGH +run_command = { version = "0.0.6", features = ["async_tokio"] } +serror = { version = "0.3.4", features = ["axum"] } +slack = { version = "0.1.0", package = "slack_client_rs" } +derive_default_builder = "0.1.8" +derive_empty_traits = "0.1.0" +merge_config_files = "0.1.5" +termination_signal = "0.1.3" +async_timing_util = "0.1.14" +partial_derive2 = "0.3.2" +derive_variants = "0.1.3" +mongo_indexed = "0.2.2" +resolver_api = "0.1.9" +parse_csl = "0.1.0" +mungos = "0.5.4" +svi = "0.1.4" + +# ASYNC +tokio = { version = "1.37.0", features = ["full"] } +reqwest = { version = "0.12.3", features = ["json"] } +tokio-util = "0.7.10" +futures = "0.3.30" +futures-util = "0.3.30" +async-trait = "0.1.80" +async-recursion = "1.1.0" + +# SERVER +axum = { version = "0.7.5", features = ["ws", "json"] } +axum-extra = { version = "0.9.3", features = ["typed-header"] } +tower = { version = "0.4.13", features = ["timeout"] } +tower-http = { version = "0.5.2", features = ["fs", "cors"] } +tokio-tungstenite = "0.21.0" + +# SER/DE +serde = { version = "1.0.198", features = ["derive"] } +strum = { version = "0.26.2", features = ["derive"] } +serde_json = "1.0.116" +toml = "0.8.12" + +# ERROR +anyhow = "1.0.82" +thiserror = "1.0.58" + +# LOGGING +opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] } +tracing-subscriber = { version = "0.3.18", features = ["json"] } +tracing-opentelemetry = "0.23.0" +opentelemetry-otlp = "0.15.0" +opentelemetry = "0.22.0" +tracing = "0.1.40" + +# CONFIG +clap = { version = "4.5.4", features = ["derive"] } +dotenv = "0.15.0" +envy = "0.4.2" + +# CRYPTO +uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } +urlencoding = "2.1.3" +rand = "0.8.5" +jwt = "0.16.0" +hmac = "0.12.1" +sha2 = "0.10.8" +bcrypt = "0.15.1" +hex = "0.4.3" + +# SYSTEM +bollard = "0.16.1" +sysinfo = "0.30.11" + +# CLOUD +aws-config = "1.2.0" +aws-sdk-ec2 = "1.34.0" + +# MISC +derive_builder = "0.20.0" +typeshare = "1.0.2" diff --git a/bin/alert_logger/Cargo.toml b/bin/alert_logger/Cargo.toml new file mode 100644 index 000000000..2b4f68391 --- /dev/null +++ b/bin/alert_logger/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "alert_logger" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +logger.workspace = true +# mogh +termination_signal.workspace = true +# external +tokio.workspace = true +tracing.workspace = true +axum.workspace = true +anyhow.workspace = true +serde.workspace = true +dotenv.workspace = true +envy.workspace = true \ No newline at end of file diff --git a/bin/alert_logger/Dockerfile b/bin/alert_logger/Dockerfile new file mode 100644 index 000000000..14441ab07 --- /dev/null +++ b/bin/alert_logger/Dockerfile @@ -0,0 +1,14 @@ +FROM rust:1.71.1 as builder +WORKDIR /builder + +COPY . . + +RUN cargo build -p alert_logger --release + +FROM gcr.io/distroless/cc + +COPY --from=builder /builder/target/release/alert_logger / + +EXPOSE 7000 + +CMD ["./alert_logger"] \ No newline at end of file diff --git a/bin/alert_logger/src/main.rs b/bin/alert_logger/src/main.rs new file mode 100644 index 000000000..2a70fd71e --- /dev/null +++ b/bin/alert_logger/src/main.rs @@ -0,0 +1,70 @@ +#[macro_use] +extern crate tracing; + +use std::{net::SocketAddr, str::FromStr}; + +use anyhow::Context; +use axum::{routing::post, Json, Router}; +use monitor_client::entities::{ + alert::Alert, server::stats::SeverityLevel, +}; +use serde::Deserialize; +use termination_signal::tokio::immediate_term_handle; + +#[derive(Deserialize)] +struct Env { + #[serde(default = "default_port")] + port: u16, +} + +fn default_port() -> u16 { + 7000 +} + +async fn app() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + logger::init(&Default::default())?; + + let Env { port } = + envy::from_env().context("failed to parse env")?; + + let socket_addr = SocketAddr::from_str(&format!("0.0.0.0:{port}")) + .context("invalid socket addr")?; + + info!("v {} | {socket_addr}", env!("CARGO_PKG_VERSION")); + + let app = Router::new().route( + "/", + post(|Json(alert): Json| async move { + if alert.resolved { + info!("Alert Resolved!: {alert:?}"); + return; + } + match alert.level { + SeverityLevel::Ok => info!("{alert:?}"), + SeverityLevel::Warning => warn!("{alert:?}"), + SeverityLevel::Critical => error!("{alert:?}"), + } + }), + ); + + let listener = tokio::net::TcpListener::bind(socket_addr) + .await + .context("failed to bind tcp listener")?; + + axum::serve(listener, app).await.context("server crashed") +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let term_signal = immediate_term_handle()?; + + let app = tokio::spawn(app()); + + tokio::select! { + res = app => return res?, + _ = term_signal => {}, + } + + Ok(()) +} diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml new file mode 100644 index 000000000..57c03cf61 --- /dev/null +++ b/bin/core/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "monitor_core" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "core" +path = "src/main.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +periphery_client.workspace = true +logger.workspace = true +# mogh +async_timing_util.workspace = true +merge_config_files.workspace = true +parse_csl.workspace = true +termination_signal.workspace = true +resolver_api.workspace = true +mungos.workspace = true +mongo_indexed.workspace = true +slack.workspace = true +serror.workspace = true +# external +tokio.workspace = true +tokio-util.workspace = true +axum.workspace = true +axum-extra.workspace = true +tower.workspace = true +tower-http.workspace = true +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +uuid.workspace = true +anyhow.workspace = true +tracing.workspace = true +dotenv.workspace = true +envy.workspace = true +reqwest.workspace = true +urlencoding.workspace = true +rand.workspace = true +jwt.workspace = true +hmac.workspace = true +sha2.workspace = true +bcrypt.workspace = true +hex.workspace = true +async-trait.workspace = true +async-recursion.workspace = true +futures.workspace = true +aws-config.workspace = true +aws-sdk-ec2.workspace = true +typeshare.workspace = true diff --git a/bin/core/Dockerfile b/bin/core/Dockerfile new file mode 100644 index 000000000..e91efd3f5 --- /dev/null +++ b/bin/core/Dockerfile @@ -0,0 +1,23 @@ +# Build Core +FROM rust:1.77.2-bullseye as core-builder +WORKDIR /builder +COPY . . +RUN cargo build -p monitor_core --release + +# Build Frontend +FROM node:20.12-alpine as frontend-builder +WORKDIR /builder +COPY ./frontend ./frontend +COPY ./client/core/ts ./client +RUN cd client && yarn && yarn build && yarn link +RUN cd frontend && yarn link @monitor/client && yarn && yarn build + +# Final Image +# FROM gcr.io/distroless/cc +FROM debian:bullseye-slim +RUN apt update && apt install -y ca-certificates +COPY ./config_example/core.config.example.toml /config/config.toml +COPY --from=core-builder /builder/target/release/core / +COPY --from=frontend-builder /builder/frontend/dist /frontend +EXPOSE 9000 +CMD ["./core"] \ No newline at end of file diff --git a/bin/core/src/api/auth.rs b/bin/core/src/api/auth.rs new file mode 100644 index 000000000..c691bed12 --- /dev/null +++ b/bin/core/src/api/auth.rs @@ -0,0 +1,132 @@ +use std::{sync::OnceLock, time::Instant}; + +use anyhow::anyhow; +use async_trait::async_trait; +use axum::{http::HeaderMap, routing::post, Router}; +use axum_extra::{headers::ContentType, TypedHeader}; +use monitor_client::{api::auth::*, entities::user::User}; +use resolver_api::{derive::Resolver, Resolve, Resolver}; +use serde::{Deserialize, Serialize}; +use serror::Json; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{ + auth::{ + get_user_id_from_headers, + github::{self, client::github_oauth_client}, + google::{self, client::google_oauth_client}, + }, + config::core_config, + helpers::query::get_user, + state::{jwt_client, State}, +}; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[resolver_target(State)] +#[resolver_args(HeaderMap)] +#[serde(tag = "type", content = "params")] +#[allow(clippy::enum_variant_names, clippy::large_enum_variant)] +pub enum AuthRequest { + GetLoginOptions(GetLoginOptions), + CreateLocalUser(CreateLocalUser), + LoginLocalUser(LoginLocalUser), + ExchangeForJwt(ExchangeForJwt), + GetUser(GetUser), +} + +pub fn router() -> Router { + let mut router = Router::new().route("/", post(handler)); + + if github_oauth_client().is_some() { + router = router.nest("/github", github::router()) + } + + if google_oauth_client().is_some() { + router = router.nest("/google", google::router()) + } + + router +} + +#[instrument(name = "AuthHandler", level = "debug", skip(headers))] +async fn handler( + headers: HeaderMap, + Json(request): Json, +) -> serror::Result<(TypedHeader, String)> { + let timer = Instant::now(); + let req_id = Uuid::new_v4(); + debug!("/auth request {req_id} | METHOD: {}", request.req_type()); + let res = State.resolve_request(request, headers).await.map_err( + |e| match e { + resolver_api::Error::Serialization(e) => { + anyhow!("{e:?}").context("response serialization error") + } + resolver_api::Error::Inner(e) => e, + }, + ); + if let Err(e) = &res { + debug!("/auth request {req_id} | error: {e:#}"); + } + let elapsed = timer.elapsed(); + debug!("/auth request {req_id} | resolve time: {elapsed:?}"); + Ok((TypedHeader(ContentType::json()), res?)) +} + +fn login_options_reponse() -> &'static GetLoginOptionsResponse { + static GET_LOGIN_OPTIONS_RESPONSE: OnceLock< + GetLoginOptionsResponse, + > = OnceLock::new(); + GET_LOGIN_OPTIONS_RESPONSE.get_or_init(|| { + let config = core_config(); + GetLoginOptionsResponse { + local: config.local_auth, + github: config.github_oauth.enabled + && !config.github_oauth.id.is_empty() + && !config.github_oauth.secret.is_empty(), + google: config.google_oauth.enabled + && !config.google_oauth.id.is_empty() + && !config.google_oauth.secret.is_empty(), + } + }) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetLoginOptions", level = "debug", skip(self))] + async fn resolve( + &self, + _: GetLoginOptions, + _: HeaderMap, + ) -> anyhow::Result { + Ok(*login_options_reponse()) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "ExchangeForJwt", level = "debug", skip(self))] + async fn resolve( + &self, + ExchangeForJwt { token }: ExchangeForJwt, + _: HeaderMap, + ) -> anyhow::Result { + let jwt = jwt_client().redeem_exchange_token(&token).await?; + let res = ExchangeForJwtResponse { jwt }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetUser", level = "debug", skip(self))] + async fn resolve( + &self, + GetUser {}: GetUser, + headers: HeaderMap, + ) -> anyhow::Result { + let user_id = get_user_id_from_headers(&headers).await?; + get_user(&user_id).await + } +} diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs new file mode 100644 index 000000000..f018c141b --- /dev/null +++ b/bin/core/src/api/execute/build.rs @@ -0,0 +1,534 @@ +use std::{str::FromStr, time::Duration}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use futures::future::join_all; +use monitor_client::{ + api::execute::{ + CancelBuild, CancelBuildResponse, Deploy, RunBuild, + }, + entities::{ + all_logs_success, + build::Build, + builder::{AwsBuilderConfig, Builder, BuilderConfig}, + deployment::DockerContainerState, + monitor_timestamp, + permission::PermissionLevel, + server::Server, + server_template::AwsServerTemplateConfig, + update::{Log, Update}, + user::{auto_redeploy_user, User}, + Operation, + }, +}; +use mungos::{ + find::find_collect, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; +use periphery_client::{ + api::{self, GetVersionResponse}, + PeripheryClient, +}; +use resolver_api::Resolve; +use serror::{serialize_error, serialize_error_pretty}; +use tokio_util::sync::CancellationToken; + +use crate::{ + cloud::{ + aws::{ + launch_ec2_instance, terminate_ec2_instance_with_retry, + Ec2Instance, + }, + BuildCleanupData, + }, + config::core_config, + helpers::{ + channel::build_cancel_channel, + periphery_client, + query::get_deployment_state, + resource::StateResource, + update::{add_update, make_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RunBuild", skip(self, user))] + async fn resolve( + &self, + RunBuild { build }: RunBuild, + user: User, + ) -> anyhow::Result { + let mut build = Build::get_resource_check_permissions( + &build, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the build (or insert default). + let action_state = + action_states().build.get_or_insert_default(&build.id).await; + + // This will set action state back to default when dropped. + // Will also check to ensure build not already busy before updating. + let _action_guard = + action_state.update(|state| state.building = true)?; + + build.config.version.increment(); + + let mut update = make_update(&build, Operation::RunBuild, &user); + update.in_progress(); + update.version = build.config.version.clone(); + + let cancel = CancellationToken::new(); + let cancel_clone = cancel.clone(); + let mut cancel_recv = + build_cancel_channel().receiver.resubscribe(); + let build_id = build.id.clone(); + + tokio::spawn(async move { + let poll = async { + loop { + let (incoming_build_id, mut update) = tokio::select! { + _ = cancel_clone.cancelled() => return Ok(()), + id = cancel_recv.recv() => id? + }; + if incoming_build_id == build_id { + info!("build cancel acknowledged"); + update.push_simple_log( + "cancel acknowledged", + "the build cancellation has been queud, it may still take some time", + ); + update.finalize(); + let id = update.id.clone(); + if let Err(e) = update_update(update).await { + warn!("failed to update Update {id} | {e:#}"); + } + cancel_clone.cancel(); + return Ok(()); + } + } + #[allow(unreachable_code)] + anyhow::Ok(()) + }; + tokio::select! { + _ = cancel_clone.cancelled() => {} + _ = poll => {} + } + }); + + update.id = add_update(update.clone()).await?; + + // GET BUILDER PERIPHERY + + let (periphery, cleanup_data) = + match get_build_builder(&build, &mut update).await { + Ok(builder) => { + info!("got builder for build"); + builder + } + Err(e) => { + warn!("failed to get builder | {e:#}"); + update.logs.push(Log::error( + "get builder", + serialize_error_pretty(&e), + )); + update.finalize(); + update_update(update.clone()).await?; + return Ok(update); + } + }; + + let core_config = core_config(); + + // CLONE REPO + + let github_token = core_config + .github_accounts + .get(&build.config.github_account) + .cloned(); + + let res = tokio::select! { + res = periphery + .request(api::git::CloneRepo { + args: (&build).into(), + github_token, + }) => res, + _ = cancel.cancelled() => { + info!("build cancelled during clone, cleaning up builder"); + update.push_error_log("build cancelled", String::from("user cancelled build during repo clone")); + cleanup_builder_instance(periphery, cleanup_data, &mut update) + .await; + info!("builder cleaned up"); + update.finalize(); + update_update(update.clone()).await?; + return Ok(update) + }, + }; + + match res { + Ok(clone_logs) => { + info!("finished repo clone"); + update.logs.extend(clone_logs); + } + Err(e) => { + warn!("failed build at clone repo | {e:#}"); + update.push_error_log("clone repo", serialize_error(&e)); + } + } + + update_update(update.clone()).await?; + + if all_logs_success(&update.logs) { + let docker_token = core_config + .docker_accounts + .get(&build.config.docker_account) + .cloned(); + + let res = tokio::select! { + res = periphery + .request(api::build::Build { + build: build.clone(), + docker_token, + }) => res.context("failed at call to periphery to build"), + _ = cancel.cancelled() => { + info!("build cancelled during build, cleaning up builder"); + update.push_error_log("build cancelled", String::from("user cancelled build during docker build")); + cleanup_builder_instance(periphery, cleanup_data, &mut update) + .await; + update.finalize(); + update_update(update.clone()).await?; + return Ok(update) + }, + }; + + match res { + Ok(logs) => { + info!("finished build"); + update.logs.extend(logs); + } + Err(e) => { + warn!("error in build | {e:#}"); + update.push_error_log("build", serialize_error(&e)) + } + }; + } + + update.finalize(); + + if update.success { + let _ = db_client() + .await + .builds + .update_one( + doc! { "_id": ObjectId::from_str(&build.id)? }, + doc! { + "$set": { + "config.version": to_bson(&build.config.version) + .context("failed at converting version to bson")?, + "info.last_built_at": monitor_timestamp(), + } + }, + None, + ) + .await; + } + + cancel.cancel(); + + cleanup_builder_instance(periphery, cleanup_data, &mut update) + .await; + + info!("builder instance cleaned up"); + + update_update(update.clone()).await?; + + if update.success { + handle_post_build_redeploy(&build.id).await; + info!("post build redeploy handled"); + } + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CancelBuild", skip(self, user))] + async fn resolve( + &self, + CancelBuild { build }: CancelBuild, + user: User, + ) -> anyhow::Result { + let build = Build::get_resource_check_permissions( + &build, + &user, + PermissionLevel::Execute, + ) + .await?; + + // check if theres already an open cancel build update + if db_client() + .await + .updates + .find_one( + doc! { + "operation": "CancelBuild", + "status": "InProgress", + "target.id": &build.id, + }, + None, + ) + .await + .context("failed to query updates")? + .is_some() + { + return Err(anyhow!("Build cancel is already in progress")); + } + + let mut update = + make_update(&build, Operation::CancelBuild, &user); + + update.push_simple_log( + "cancel triggered", + "the build cancel has been triggered", + ); + update.in_progress(); + + update.id = + add_update(make_update(&build, Operation::CancelBuild, &user)) + .await?; + + build_cancel_channel() + .sender + .lock() + .await + .send((build.id, update))?; + + Ok(CancelBuildResponse {}) + } +} + +const BUILDER_POLL_RATE_SECS: u64 = 2; +const BUILDER_POLL_MAX_TRIES: usize = 30; + +#[instrument] +async fn get_build_builder( + build: &Build, + update: &mut Update, +) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { + if build.config.builder_id.is_empty() { + return Err(anyhow!("build has not configured a builder")); + } + let builder = + Builder::get_resource(&build.config.builder_id).await?; + match builder.config { + BuilderConfig::Server(config) => { + if config.server_id.is_empty() { + return Err(anyhow!("builder has not configured a server")); + } + let server = Server::get_resource(&config.server_id).await?; + let periphery = periphery_client(&server)?; + Ok(( + periphery, + BuildCleanupData::Server { + repo_name: build.name.clone(), + }, + )) + } + BuilderConfig::Aws(config) => { + get_aws_builder(build, config, update).await + } + } +} + +#[instrument] +async fn get_aws_builder( + build: &Build, + config: AwsBuilderConfig, + update: &mut Update, +) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { + let start_create_ts = monitor_timestamp(); + + let instance_name = format!( + "BUILDER-{}-v{}", + build.name, + build.config.version.to_string() + ); + let Ec2Instance { instance_id, ip } = launch_ec2_instance( + &instance_name, + AwsServerTemplateConfig::from_builder_config(&config), + ) + .await?; + + info!("ec2 instance launched"); + + let log = Log { + stage: "start build instance".to_string(), + success: true, + stdout: start_aws_builder_log(&instance_id, &ip, &config), + start_ts: start_create_ts, + end_ts: monitor_timestamp(), + ..Default::default() + }; + + update.logs.push(log); + + update_update(update.clone()).await?; + + let periphery_address = format!("http://{ip}:{}", config.port); + let periphery = + PeripheryClient::new(&periphery_address, &core_config().passkey); + + let start_connect_ts = monitor_timestamp(); + let mut res = Ok(GetVersionResponse { + version: String::new(), + }); + for _ in 0..BUILDER_POLL_MAX_TRIES { + let version = periphery + .request(api::GetVersion {}) + .await + .context("failed to reach periphery client on builder"); + if let Ok(GetVersionResponse { version }) = &version { + let connect_log = Log { + stage: "build instance connected".to_string(), + success: true, + stdout: format!( + "established contact with periphery on builder\nperiphery version: v{}", + version + ), + start_ts: start_connect_ts, + end_ts: monitor_timestamp(), + ..Default::default() + }; + update.logs.push(connect_log); + update_update(update.clone()).await?; + return Ok(( + periphery, + BuildCleanupData::Aws { + instance_id, + region: config.region, + }, + )); + } + res = version; + tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS)) + .await; + } + tokio::spawn(async move { + let _ = + terminate_ec2_instance_with_retry(config.region, &instance_id) + .await; + }); + + // Unwrap is safe, only way to get here is after check Ok / early return, so it must be err + Err(res.err().unwrap()) +} + +#[instrument(skip(periphery))] +async fn cleanup_builder_instance( + periphery: PeripheryClient, + cleanup_data: BuildCleanupData, + update: &mut Update, +) { + match cleanup_data { + BuildCleanupData::Server { repo_name } => { + let _ = periphery + .request(api::git::DeleteRepo { name: repo_name }) + .await; + } + BuildCleanupData::Aws { + instance_id, + region, + } => { + let _instance_id = instance_id.clone(); + tokio::spawn(async move { + let _ = + terminate_ec2_instance_with_retry(region, &_instance_id) + .await; + }); + update.push_simple_log( + "terminate instance", + format!("termination queued for instance id {instance_id}"), + ); + } + } +} + +#[instrument] +async fn handle_post_build_redeploy(build_id: &str) { + let Ok(redeploy_deployments) = find_collect( + &db_client().await.deployments, + doc! { + "config.image.params.build_id": build_id, + "config.redeploy_on_build": true + }, + None, + ) + .await + else { + return; + }; + + let futures = + redeploy_deployments + .into_iter() + .map(|deployment| async move { + let state = + get_deployment_state(&deployment).await.unwrap_or_default(); + if state == DockerContainerState::Running { + let res = State + .resolve( + Deploy { + deployment: deployment.id.clone(), + stop_signal: None, + stop_time: None, + }, + auto_redeploy_user().to_owned(), + ) + .await; + Some((deployment.id.clone(), res)) + } else { + None + } + }); + + let redeploy_results = join_all(futures).await; + + let mut redeploys = Vec::::new(); + let mut redeploy_failures = Vec::::new(); + + for res in redeploy_results { + if res.is_none() { + continue; + } + let (id, res) = res.unwrap(); + match res { + Ok(_) => redeploys.push(id), + Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")), + } + } +} + +fn start_aws_builder_log( + instance_id: &str, + ip: &str, + config: &AwsBuilderConfig, +) -> String { + let AwsBuilderConfig { + ami_id, + instance_type, + volume_gb, + subnet_id, + assign_public_ip, + security_group_ids, + use_public_ip, + .. + } = config; + + let readable_sec_group_ids = security_group_ids.join(", "); + + format!("instance id: {instance_id}\nip: {ip}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}\nassign public ip: {assign_public_ip}\nuse public ip: {use_public_ip}") +} diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs new file mode 100644 index 000000000..d911fae6a --- /dev/null +++ b/bin/core/src/api/execute/deployment.rs @@ -0,0 +1,455 @@ +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use futures::future::join_all; +use monitor_client::{ + api::execute::*, + entities::{ + build::Build, + deployment::{Deployment, DeploymentImage}, + get_image_name, monitor_timestamp, + permission::PermissionLevel, + server::ServerStatus, + update::{Log, ResourceTarget, Update, UpdateStatus}, + user::User, + Operation, Version, + }, +}; +use mungos::{find::find_collect, mongodb::bson::doc}; +use periphery_client::api; +use resolver_api::Resolve; +use serror::serialize_error_pretty; + +use crate::{ + config::core_config, + helpers::{ + periphery_client, + query::get_server_with_status, + resource::StateResource, + update::{add_update, make_update, update_update}, + }, + monitor::update_cache_for_server, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "Deploy", skip(self, user))] + async fn resolve( + &self, + Deploy { + deployment, + stop_signal, + stop_time, + }: Deploy, + user: User, + ) -> anyhow::Result { + let mut deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.deploying = true)?; + + if deployment.config.server_id.is_empty() { + return Err(anyhow!("deployment has no server configured")); + } + + let (server, status) = + get_server_with_status(&deployment.config.server_id).await?; + if status != ServerStatus::Ok { + return Err(anyhow!( + "cannot send action when server is unreachable or disabled" + )); + } + + let periphery = periphery_client(&server)?; + + let version = match deployment.config.image { + DeploymentImage::Build { build_id, version } => { + let build = Build::get_resource(&build_id).await?; + let image_name = get_image_name(&build); + let version = if version.is_none() { + build.config.version + } else { + version + }; + deployment.config.image = DeploymentImage::Image { + image: format!("{image_name}:{}", version.to_string()), + }; + if deployment.config.docker_account.is_empty() { + deployment.config.docker_account = + build.config.docker_account; + } + version + } + DeploymentImage::Image { .. } => Version::default(), + }; + + let mut update = + make_update(&deployment, Operation::DeployContainer, &user); + update.in_progress(); + update.version = version; + + update.id = add_update(update.clone()).await?; + + let docker_token = core_config() + .docker_accounts + .get(&deployment.config.docker_account) + .cloned(); + + match periphery + .request(api::container::Deploy { + deployment, + stop_signal, + stop_time, + docker_token, + }) + .await + { + Ok(log) => update.logs.push(log), + Err(e) => { + update.push_error_log( + "deploy container", + serialize_error_pretty(&e), + ); + } + }; + + update_cache_for_server(&server).await; + + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "StartContainer", skip(self, user))] + async fn resolve( + &self, + StartContainer { deployment }: StartContainer, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.starting = true)?; + + if deployment.config.server_id.is_empty() { + return Err(anyhow!("deployment has no server configured")); + } + + let (server, status) = + get_server_with_status(&deployment.config.server_id).await?; + if status != ServerStatus::Ok { + return Err(anyhow!( + "cannot send action when server is unreachable or disabled" + )); + } + + let periphery = periphery_client(&server)?; + + let start_ts = monitor_timestamp(); + + let mut update = Update { + target: ResourceTarget::Deployment(deployment.id.clone()), + operation: Operation::StartContainer, + start_ts, + status: UpdateStatus::InProgress, + success: true, + operator: user.id.clone(), + ..Default::default() + }; + + update.id = add_update(update.clone()).await?; + + let log = match periphery + .request(api::container::StartContainer { + name: deployment.name.clone(), + }) + .await + { + Ok(log) => log, + Err(e) => { + Log::error("start container", serialize_error_pretty(&e)) + } + }; + + update.logs.push(log); + update.finalize(); + update_cache_for_server(&server).await; + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "StopContainer", skip(self, user))] + async fn resolve( + &self, + StopContainer { + deployment, + signal, + time, + }: StopContainer, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.stopping = true)?; + + if deployment.config.server_id.is_empty() { + return Err(anyhow!("deployment has no server configured")); + } + + let (server, status) = + get_server_with_status(&deployment.config.server_id).await?; + if status != ServerStatus::Ok { + return Err(anyhow!( + "cannot send action when server is unreachable or disabled" + )); + } + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&deployment, Operation::StopContainer, &user); + + update.id = add_update(update.clone()).await?; + + let log = match periphery + .request(api::container::StopContainer { + name: deployment.name.clone(), + signal: signal + .unwrap_or(deployment.config.termination_signal) + .into(), + time: time + .unwrap_or(deployment.config.termination_timeout) + .into(), + }) + .await + { + Ok(log) => log, + Err(e) => { + Log::error("stop container", serialize_error_pretty(&e)) + } + }; + + update.logs.push(log); + update.finalize(); + update_cache_for_server(&server).await; + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "StopAllContainers", skip(self, user))] + async fn resolve( + &self, + StopAllContainers { server }: StopAllContainers, + user: User, + ) -> anyhow::Result { + let (server, status) = get_server_with_status(&server).await?; + if status != ServerStatus::Ok { + return Err(anyhow!( + "cannot send action when server is unreachable or disabled" + )); + } + + // get the action state for the server (or insert default). + let action_state = action_states() + .server + .get_or_insert_default(&server.id) + .await; + + // Will check to ensure server not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = action_state + .update(|state| state.stopping_containers = true)?; + + let deployments = find_collect( + &db_client().await.deployments, + doc! { + "config.server_id": &server.id + }, + None, + ) + .await + .context("failed to find deployments on server")?; + + let mut update = + make_update(&server, Operation::StopAllContainers, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let futures = deployments.iter().map(|deployment| async { + ( + self + .resolve( + StopContainer { + deployment: deployment.id.clone(), + signal: None, + time: None, + }, + user.clone(), + ) + .await, + deployment.name.clone(), + deployment.id.clone(), + ) + }); + let results = join_all(futures).await; + let deployment_names = deployments + .iter() + .map(|d| format!("{} ({})", d.name, d.id)) + .collect::>() + .join("\n"); + update.push_simple_log("stopping containers", deployment_names); + for (res, name, id) in results { + if let Err(e) = res { + update.push_error_log( + "stop container failure", + format!( + "failed to stop container {name} ({id})\n\n{}", + serialize_error_pretty(&e) + ), + ); + } + } + + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RemoveContainer", skip(self, user))] + async fn resolve( + &self, + RemoveContainer { + deployment, + signal, + time, + }: RemoveContainer, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.removing = true)?; + + if deployment.config.server_id.is_empty() { + return Err(anyhow!("deployment has no server configured")); + } + + let (server, status) = + get_server_with_status(&deployment.config.server_id).await?; + if status != ServerStatus::Ok { + return Err(anyhow!( + "cannot send action when server is unreachable or disabled" + )); + } + + let periphery = periphery_client(&server)?; + + let start_ts = monitor_timestamp(); + + let mut update = Update { + target: ResourceTarget::Deployment(deployment.id.clone()), + operation: Operation::RemoveContainer, + start_ts, + status: UpdateStatus::InProgress, + success: true, + operator: user.id.clone(), + ..Default::default() + }; + + update.id = add_update(update.clone()).await?; + + let log = match periphery + .request(api::container::RemoveContainer { + name: deployment.name.clone(), + signal: signal + .unwrap_or(deployment.config.termination_signal) + .into(), + time: time + .unwrap_or(deployment.config.termination_timeout) + .into(), + }) + .await + { + Ok(log) => log, + Err(e) => { + Log::error("stop container", serialize_error_pretty(&e)) + } + }; + + update.logs.push(log); + update.finalize(); + update_cache_for_server(&server).await; + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs new file mode 100644 index 000000000..9cdefadcf --- /dev/null +++ b/bin/core/src/api/execute/mod.rs @@ -0,0 +1,109 @@ +use std::time::Instant; + +use anyhow::{anyhow, Context}; +use axum::{middleware, routing::post, Extension, Router}; +use axum_extra::{headers::ContentType, TypedHeader}; +use monitor_client::{api::execute::*, entities::user::User}; +use resolver_api::{derive::Resolver, Resolve, Resolver}; +use serde::{Deserialize, Serialize}; +use serror::Json; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{auth::auth_request, state::State}; + +mod build; +mod deployment; +mod procedure; +mod repo; +mod server; +mod server_template; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[resolver_target(State)] +#[resolver_args(User)] +#[serde(tag = "type", content = "params")] +enum ExecuteRequest { + // ==== SERVER ==== + PruneContainers(PruneDockerContainers), + PruneImages(PruneDockerImages), + PruneNetworks(PruneDockerNetworks), + + // ==== DEPLOYMENT ==== + Deploy(Deploy), + StartContainer(StartContainer), + StopContainer(StopContainer), + StopAllContainers(StopAllContainers), + RemoveContainer(RemoveContainer), + + // ==== BUILD ==== + RunBuild(RunBuild), + CancelBuild(CancelBuild), + + // ==== REPO ==== + CloneRepo(CloneRepo), + PullRepo(PullRepo), + + // ==== PROCEDURE ==== + RunProcedure(RunProcedure), + + // ==== SERVER TEMPLATE ==== + LaunchServer(LaunchServer), +} + +pub fn router() -> Router { + Router::new() + .route("/", post(handler)) + .layer(middleware::from_fn(auth_request)) +} + +async fn handler( + Extension(user): Extension, + Json(request): Json, +) -> serror::Result<(TypedHeader, String)> { + let req_id = Uuid::new_v4(); + + let res = tokio::spawn(task(req_id, request, user)) + .await + .context("failure in spawned execute task"); + + if let Err(e) = &res { + warn!("/execute request {req_id} spawn error: {e:#}",); + } + + Ok((TypedHeader(ContentType::json()), res??)) +} + +#[instrument(name = "ExecuteRequest", skip(user))] +async fn task( + req_id: Uuid, + request: ExecuteRequest, + user: User, +) -> anyhow::Result { + info!( + "/execute request {req_id} | user: {} ({})", + user.username, user.id + ); + let timer = Instant::now(); + + let res = + State + .resolve_request(request, user) + .await + .map_err(|e| match e { + resolver_api::Error::Serialization(e) => { + anyhow!("{e:?}").context("response serialization error") + } + resolver_api::Error::Inner(e) => e, + }); + + if let Err(e) = &res { + warn!("/execute request {req_id} error: {e:#}"); + } + + let elapsed = timer.elapsed(); + info!("/execute request {req_id} | resolve time: {elapsed:?}"); + + res +} diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs new file mode 100644 index 000000000..f81c7eec6 --- /dev/null +++ b/bin/core/src/api/execute/procedure.rs @@ -0,0 +1,83 @@ +use async_trait::async_trait; +use monitor_client::{ + api::execute::RunProcedure, + entities::{ + permission::PermissionLevel, procedure::Procedure, + update::Update, user::User, Operation, + }, +}; +use resolver_api::Resolve; +use serror::serialize_error_pretty; +use tokio::sync::Mutex; + +use crate::{ + helpers::{ + procedure::execute_procedure, + resource::StateResource, + update::{add_update, make_update, update_update}, + }, + state::{action_states, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RunProcedure", skip(self, user))] + async fn resolve( + &self, + RunProcedure { procedure }: RunProcedure, + user: User, + ) -> anyhow::Result { + let procedure = Procedure::get_resource_check_permissions( + &procedure, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the procedure (or insert default). + let action_state = action_states() + .procedure + .get_or_insert_default(&procedure.id) + .await; + + // This will set action state back to default when dropped. + // Will also check to ensure procedure not already busy before updating. + let _action_guard = + action_state.update(|state| state.running = true)?; + + let mut update = + make_update(&procedure, Operation::RunProcedure, &user); + update.in_progress(); + update.push_simple_log( + "execute procedure", + format!("Executing procedure: {}", procedure.name), + ); + + update.id = add_update(update.clone()).await?; + + let update = Mutex::new(update); + + let res = execute_procedure(&procedure, &update).await; + + let mut update = update.into_inner(); + + match res { + Ok(_) => { + update.push_simple_log( + "execution ok", + "the procedure has completed with no errors", + ); + } + Err(e) => update.push_error_log( + "execution error", + serialize_error_pretty(&e), + ), + } + + update.finalize(); + + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs new file mode 100644 index 000000000..0f68258d0 --- /dev/null +++ b/bin/core/src/api/execute/repo.rs @@ -0,0 +1,205 @@ +use std::str::FromStr; + +use anyhow::anyhow; +use async_trait::async_trait; +use monitor_client::{ + api::execute::*, + entities::{ + monitor_timestamp, optional_string, + permission::PermissionLevel, + repo::Repo, + server::Server, + update::{Log, ResourceTarget, Update, UpdateStatus}, + user::User, + Operation, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use periphery_client::api; +use resolver_api::Resolve; +use serror::serialize_error_pretty; + +use crate::{ + config::core_config, + helpers::{ + periphery_client, + resource::StateResource, + update::{add_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CloneRepo", skip(self, user))] + async fn resolve( + &self, + CloneRepo { repo }: CloneRepo, + user: User, + ) -> anyhow::Result { + let repo = Repo::get_resource_check_permissions( + &repo, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the repo (or insert default). + let action_state = + action_states().repo.get_or_insert_default(&repo.id).await; + + // This will set action state back to default when dropped. + // Will also check to ensure repo not already busy before updating. + let _action_guard = + action_state.update(|state| state.cloning = true)?; + + if repo.config.server_id.is_empty() { + return Err(anyhow!("repo has no server attached")); + } + + let server = Server::get_resource(&repo.config.server_id).await?; + + let periphery = periphery_client(&server)?; + + let start_ts = monitor_timestamp(); + + let mut update = Update { + operation: Operation::CloneRepo, + target: ResourceTarget::Repo(repo.id.clone()), + start_ts, + status: UpdateStatus::InProgress, + operator: user.id.clone(), + success: true, + ..Default::default() + }; + + update.id = add_update(update.clone()).await?; + + let github_token = core_config() + .github_accounts + .get(&repo.config.github_account) + .cloned(); + + let logs = match periphery + .request(api::git::CloneRepo { + args: (&repo).into(), + github_token, + }) + .await + { + Ok(logs) => logs, + Err(e) => { + vec![Log::error("clone repo", serialize_error_pretty(&e))] + } + }; + + update.logs.extend(logs); + update.finalize(); + + if update.success { + let res = db_client().await + .repos + .update_one( + doc! { "_id": ObjectId::from_str(&repo.id)? }, + doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } }, + None, + ) + .await; + if let Err(e) = res { + warn!( + "failed to update repo last_pulled_at | repo id: {} | {e:#}", + repo.id + ); + } + } + + update_update(update.clone()).await?; + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PullRepo", skip(self, user))] + async fn resolve( + &self, + PullRepo { repo }: PullRepo, + user: User, + ) -> anyhow::Result { + let repo = Repo::get_resource_check_permissions( + &repo, + &user, + PermissionLevel::Write, + ) + .await?; + + // get the action state for the repo (or insert default). + let action_state = + action_states().repo.get_or_insert_default(&repo.id).await; + + // This will set action state back to default when dropped. + // Will also check to ensure repo not already busy before updating. + let _action_guard = + action_state.update(|state| state.pulling = true)?; + + if repo.config.server_id.is_empty() { + return Err(anyhow!("repo has no server attached")); + } + + let server = Server::get_resource(&repo.config.server_id).await?; + + let periphery = periphery_client(&server)?; + + let start_ts = monitor_timestamp(); + + let mut update = Update { + operation: Operation::PullRepo, + target: ResourceTarget::Repo(repo.id.clone()), + start_ts, + status: UpdateStatus::InProgress, + operator: user.id.clone(), + success: true, + ..Default::default() + }; + + update.id = add_update(update.clone()).await?; + + let logs = match periphery + .request(api::git::PullRepo { + name: repo.name, + branch: optional_string(&repo.config.branch), + on_pull: repo.config.on_pull.into_option(), + }) + .await + { + Ok(logs) => logs, + Err(e) => { + vec![Log::error("pull repo", serialize_error_pretty(&e))] + } + }; + + update.logs.extend(logs); + + update.finalize(); + + if update.success { + let res = db_client().await + .repos + .update_one( + doc! { "_id": ObjectId::from_str(&repo.id)? }, + doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } }, + None, + ) + .await; + if let Err(e) = res { + warn!( + "failed to update repo last_pulled_at | repo id: {} | {e:#}", + repo.id + ); + } + } + + update_update(update.clone()).await?; + Ok(update) + } +} diff --git a/bin/core/src/api/execute/server.rs b/bin/core/src/api/execute/server.rs new file mode 100644 index 000000000..6f0f7c11c --- /dev/null +++ b/bin/core/src/api/execute/server.rs @@ -0,0 +1,194 @@ +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::execute::*, + entities::{ + monitor_timestamp, + permission::PermissionLevel, + server::Server, + update::{Log, Update, UpdateStatus}, + user::User, + Operation, + }, +}; +use periphery_client::api; +use resolver_api::Resolve; +use serror::serialize_error_pretty; + +use crate::{ + helpers::{ + periphery_client, + resource::StateResource, + update::{add_update, make_update, update_update}, + }, + state::{action_states, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneDockerContainers", skip(self, user))] + async fn resolve( + &self, + PruneDockerContainers { server }: PruneDockerContainers, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the server (or insert default). + let action_state = action_states() + .server + .get_or_insert_default(&server.id) + .await; + + // Will check to ensure server not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.pruning_containers = true)?; + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&server, Operation::PruneContainersServer, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let log = match periphery + .request(api::container::PruneContainers {}) + .await + .context(format!( + "failed to prune containers on server {}", + server.name + )) { + Ok(log) => log, + Err(e) => { + Log::error("prune containers", serialize_error_pretty(&e)) + } + }; + + update.success = log.success; + update.status = UpdateStatus::Complete; + update.end_ts = Some(monitor_timestamp()); + update.logs.push(log); + + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneDockerNetworks", skip(self, user))] + async fn resolve( + &self, + PruneDockerNetworks { server }: PruneDockerNetworks, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the server (or insert default). + let action_state = action_states() + .server + .get_or_insert_default(&server.id) + .await; + + // Will check to ensure server not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.pruning_networks = true)?; + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&server, Operation::PruneNetworksServer, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let log = match periphery + .request(api::network::PruneNetworks {}) + .await + .context(format!( + "failed to prune networks on server {}", + server.name + )) { + Ok(log) => log, + Err(e) => { + Log::error("prune networks", serialize_error_pretty(&e)) + } + }; + + update.success = log.success; + update.status = UpdateStatus::Complete; + update.end_ts = Some(monitor_timestamp()); + update.logs.push(log); + + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneDockerImages", skip(self, user))] + async fn resolve( + &self, + PruneDockerImages { server }: PruneDockerImages, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Execute, + ) + .await?; + + // get the action state for the server (or insert default). + let action_state = action_states() + .server + .get_or_insert_default(&server.id) + .await; + + // Will check to ensure server not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.pruning_images = true)?; + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&server, Operation::PruneImagesServer, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let log = + match periphery.request(api::build::PruneImages {}).await { + Ok(log) => log, + Err(e) => Log::error( + "prune images", + format!( + "failed to prune images on server {} | {e:#?}", + server.name + ), + ), + }; + + update.logs.push(log); + + update.finalize(); + + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/server_template.rs b/bin/core/src/api/execute/server_template.rs new file mode 100644 index 000000000..21008775f --- /dev/null +++ b/bin/core/src/api/execute/server_template.rs @@ -0,0 +1,89 @@ +use axum::async_trait; +use monitor_client::{ + api::{execute::LaunchServer, write::CreateServer}, + entities::{ + permission::PermissionLevel, + server::PartialServerConfig, + server_template::{ServerTemplate, ServerTemplateConfig}, + update::Update, + user::User, + Operation, + }, +}; +use resolver_api::Resolve; + +use crate::{ + cloud::aws::launch_ec2_instance, + helpers::{ + resource::StateResource, + update::{add_update, make_update, update_update}, + }, + state::State, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "LaunchServer", skip(self, user))] + async fn resolve( + &self, + LaunchServer { + name, + server_template, + }: LaunchServer, + user: User, + ) -> anyhow::Result { + let template = ServerTemplate::get_resource_check_permissions( + &server_template, + &user, + PermissionLevel::Execute, + ) + .await?; + let mut update = + make_update(&template, Operation::LaunchServer, &user); + update.push_simple_log( + "launching server", + format!("{:#?}", template.config), + ); + update.id = add_update(update.clone()).await?; + match template.config { + ServerTemplateConfig::Aws(config) => { + let region = config.region.clone(); + let instance = launch_ec2_instance(&name, config).await; + if let Err(e) = &instance { + update.push_error_log( + "launch server", + format!("failed to launch aws instance\n\n{e:#?}"), + ); + update.finalize(); + update_update(update.clone()).await?; + return Ok(update); + } + let instance = instance.unwrap(); + update.push_simple_log( + "launch server", + format!( + "successfully launched server {name} on ip {}", + instance.ip + ), + ); + let _ = self + .resolve( + CreateServer { + name, + config: PartialServerConfig { + address: format!("http://{}:8120", instance.ip) + .into(), + region: region.into(), + ..Default::default() + }, + }, + user, + ) + .await; + } + } + update.finalize(); + update_update(update.clone()).await?; + Ok(update) + } +} diff --git a/bin/core/src/api/mod.rs b/bin/core/src/api/mod.rs new file mode 100644 index 000000000..55f891e40 --- /dev/null +++ b/bin/core/src/api/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod execute; +pub mod read; +pub mod write; diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs new file mode 100644 index 000000000..1ecf91039 --- /dev/null +++ b/bin/core/src/api/read/alert.rs @@ -0,0 +1,80 @@ +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse, + }, + entities::{deployment::Deployment, server::Server, user::User}, +}; +use mungos::{ + by_id::find_one_by_id, + find::find_collect, + mongodb::{bson::doc, options::FindOptions}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::StateResource, + state::{db_client, State}, +}; + +const NUM_ALERTS_PER_PAGE: u64 = 20; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListAlerts { query, page }: ListAlerts, + user: User, + ) -> anyhow::Result { + let mut query = query.unwrap_or_default(); + if !user.admin { + let server_ids = + Server::get_resource_ids_for_non_admin(&user.id).await?; + let deployment_ids = + Deployment::get_resource_ids_for_non_admin(&user.id).await?; + query.extend(doc! { + "$or": [ + { "target.type": "Server", "target.id": { "$in": &server_ids } }, + { "target.type": "Deployment", "target.id": { "$in": &deployment_ids } }, + ] + }); + } + + let alerts = find_collect( + &db_client().await.alerts, + query, + FindOptions::builder() + .sort(doc! { "ts": -1 }) + .limit(NUM_ALERTS_PER_PAGE as i64) + .skip(page * NUM_ALERTS_PER_PAGE) + .build(), + ) + .await + .context("failed to get alerts from db")?; + + let next_page = if alerts.len() < NUM_ALERTS_PER_PAGE as usize { + None + } else { + Some((page + 1) as i64) + }; + + let res = ListAlertsResponse { next_page, alerts }; + + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAlert { id }: GetAlert, + _: User, + ) -> anyhow::Result { + find_one_by_id(&db_client().await.alerts, &id) + .await + .context("failed to query db for alert")? + .context("no alert found with given id") + } +} diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs new file mode 100644 index 000000000..87fbe8599 --- /dev/null +++ b/bin/core/src/api/read/alerter.rs @@ -0,0 +1,85 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::*, + entities::{ + alerter::{Alerter, AlerterListItem}, + permission::PermissionLevel, + update::ResourceTargetVariant, + user::User, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAlerter { alerter }: GetAlerter, + user: User, + ) -> anyhow::Result { + Alerter::get_resource_check_permissions( + &alerter, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListAlerters { query }: ListAlerters, + user: User, + ) -> anyhow::Result> { + Alerter::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAlertersSummary {}: GetAlertersSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Alerter, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .alerters + .count_documents(query, None) + .await + .context("failed to count all alerter documents")?; + let res = GetAlertersSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs new file mode 100644 index 000000000..1d64a0f16 --- /dev/null +++ b/bin/core/src/api/read/build.rs @@ -0,0 +1,262 @@ +use std::{collections::HashMap, str::FromStr, sync::OnceLock}; + +use anyhow::Context; +use async_timing_util::unix_timestamp_ms; +use async_trait::async_trait; +use futures::TryStreamExt; +use monitor_client::{ + api::read::*, + entities::{ + build::{Build, BuildActionState, BuildListItem}, + permission::PermissionLevel, + update::{ResourceTargetVariant, UpdateStatus}, + user::User, + Operation, + }, +}; +use mungos::{ + find::find_collect, + mongodb::{ + bson::{doc, oid::ObjectId}, + options::FindOptions, + }, +}; +use resolver_api::{Resolve, ResolveToString}; + +use crate::{ + config::core_config, + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuild { build }: GetBuild, + user: User, + ) -> anyhow::Result { + Build::get_resource_check_permissions( + &build, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListBuilds { query }: ListBuilds, + user: User, + ) -> anyhow::Result> { + Build::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildActionState { build }: GetBuildActionState, + user: User, + ) -> anyhow::Result { + let build = Build::get_resource_check_permissions( + &build, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = action_states() + .build + .get(&build.id) + .await + .unwrap_or_default() + .get()?; + Ok(action_state) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildsSummary {}: GetBuildsSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Build, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .builds + .count_documents(query, None) + .await + .context("failed to count all build documents")?; + let res = GetBuildsSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} + +const ONE_DAY_MS: i64 = 86400000; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildMonthlyStats { page }: GetBuildMonthlyStats, + _: User, + ) -> anyhow::Result { + let curr_ts = unix_timestamp_ms() as i64; + let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; + + let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS; + let open_ts = close_ts - 30 * ONE_DAY_MS; + + let mut build_updates = db_client() + .await + .updates + .find( + doc! { + "start_ts": { + "$gte": open_ts, + "$lt": close_ts + }, + "operation": Operation::RunBuild.to_string(), + }, + None, + ) + .await + .context("failed to get updates cursor")?; + + let mut days = HashMap::::with_capacity(32); + + let mut curr = open_ts; + + while curr < close_ts { + let stats = BuildStatsDay { + ts: curr as f64, + ..Default::default() + }; + days.insert(curr, stats); + curr += ONE_DAY_MS; + } + + while let Some(update) = build_updates.try_next().await? { + if let Some(end_ts) = update.end_ts { + let day = update.start_ts - update.start_ts % ONE_DAY_MS; + let entry = days.entry(day).or_default(); + entry.count += 1.0; + entry.time += ms_to_hour(end_ts - update.start_ts); + } + } + + Ok(GetBuildMonthlyStatsResponse::new( + days.into_values().collect(), + )) + } +} + +const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0; +fn ms_to_hour(duration: i64) -> f64 { + duration as f64 / MS_TO_HOUR_DIVISOR +} + +const NUM_VERSIONS_PER_PAGE: u64 = 10; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildVersions { + build, + page, + major, + minor, + patch, + }: GetBuildVersions, + user: User, + ) -> anyhow::Result> { + let build = Build::get_resource_check_permissions( + &build, + &user, + PermissionLevel::Read, + ) + .await?; + + let mut filter = doc! { + "target": { + "type": "Build", + "id": build.id + }, + "operation": Operation::RunBuild.to_string(), + "status": UpdateStatus::Complete.to_string(), + "success": true + }; + if let Some(major) = major { + filter.insert("version.major", major); + } + if let Some(minor) = minor { + filter.insert("version.minor", minor); + } + if let Some(patch) = patch { + filter.insert("version.patch", patch); + } + + let versions = find_collect( + &db_client().await.updates, + filter, + FindOptions::builder() + .sort(doc! { "_id": -1 }) + .limit(NUM_VERSIONS_PER_PAGE as i64) + .skip(page as u64 * NUM_VERSIONS_PER_PAGE) + .build(), + ) + .await + .context("failed to pull versions from mongo")? + .into_iter() + .map(|u| (u.version, u.start_ts)) + .filter(|(v, _)| !v.is_none()) + .map(|(version, ts)| BuildVersionResponseItem { version, ts }) + .collect(); + Ok(versions) + } +} + +fn docker_organizations() -> &'static String { + static DOCKER_ORGANIZATIONS: OnceLock = OnceLock::new(); + DOCKER_ORGANIZATIONS.get_or_init(|| { + serde_json::to_string(&core_config().docker_organizations) + .expect("failed to serialize docker organizations") + }) +} + +#[async_trait] +impl ResolveToString for State { + async fn resolve_to_string( + &self, + ListDockerOrganizations {}: ListDockerOrganizations, + _: User, + ) -> anyhow::Result { + Ok(docker_organizations().clone()) + } +} diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs new file mode 100644 index 000000000..33e6faf64 --- /dev/null +++ b/bin/core/src/api/read/builder.rs @@ -0,0 +1,123 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{self, *}, + entities::{ + builder::{Builder, BuilderConfig, BuilderListItem}, + permission::PermissionLevel, + update::ResourceTargetVariant, + user::User, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuilder { builder }: GetBuilder, + user: User, + ) -> anyhow::Result { + Builder::get_resource_check_permissions( + &builder, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListBuilders { query }: ListBuilders, + user: User, + ) -> anyhow::Result> { + Builder::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildersSummary {}: GetBuildersSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Builder, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .builders + .count_documents(query, None) + .await + .context("failed to count all builder documents")?; + let res = GetBuildersSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuilderAvailableAccounts { builder }: GetBuilderAvailableAccounts, + user: User, + ) -> anyhow::Result { + let builder = Builder::get_resource_check_permissions( + &builder, + &user, + PermissionLevel::Read, + ) + .await?; + match builder.config { + BuilderConfig::Aws(config) => { + Ok(GetBuilderAvailableAccountsResponse { + github: config.github_accounts, + docker: config.docker_accounts, + }) + } + BuilderConfig::Server(config) => { + let res = self + .resolve( + read::GetAvailableAccounts { + server: config.server_id, + }, + user, + ) + .await?; + Ok(GetBuilderAvailableAccountsResponse { + github: res.github, + docker: res.docker, + }) + } + } + } +} diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs new file mode 100644 index 000000000..7e67c5926 --- /dev/null +++ b/bin/core/src/api/read/deployment.rs @@ -0,0 +1,281 @@ +use std::{cmp, collections::HashSet, str::FromStr}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::read::*, + entities::{ + deployment::{ + Deployment, DeploymentActionState, DeploymentConfig, + DeploymentListItem, DockerContainerState, DockerContainerStats, + }, + permission::PermissionLevel, + server::Server, + update::{Log, ResourceTargetVariant}, + user::User, + }, +}; +use mungos::{ + find::find_collect, + mongodb::bson::{doc, oid::ObjectId}, +}; +use periphery_client::api; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + periphery_client, + resource::{get_resource_ids_for_non_admin, StateResource}, + }, + state::{action_states, db_client, deployment_status_cache, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDeployment { deployment }: GetDeployment, + user: User, + ) -> anyhow::Result { + Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListDeployments { query }: ListDeployments, + user: User, + ) -> anyhow::Result> { + Deployment::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDeploymentContainer { deployment }: GetDeploymentContainer, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await?; + let status = deployment_status_cache() + .get(&deployment.id) + .await + .unwrap_or_default(); + let response = GetDeploymentContainerResponse { + state: status.curr.state, + container: status.curr.container.clone(), + }; + Ok(response) + } +} + +const MAX_LOG_LENGTH: u64 = 5000; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetLog { deployment, tail }: GetLog, + user: User, + ) -> anyhow::Result { + let Deployment { + name, + config: DeploymentConfig { server_id, .. }, + .. + } = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await?; + if server_id.is_empty() { + return Ok(Log::default()); + } + let server = Server::get_resource(&server_id).await?; + periphery_client(&server)? + .request(api::container::GetContainerLog { + name, + tail: cmp::min(tail, MAX_LOG_LENGTH), + }) + .await + .context("failed at call to periphery") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + SearchLog { + deployment, + terms, + combinator, + }: SearchLog, + user: User, + ) -> anyhow::Result { + let Deployment { + name, + config: DeploymentConfig { server_id, .. }, + .. + } = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await?; + if server_id.is_empty() { + return Ok(Log::default()); + } + let server = Server::get_resource(&server_id).await?; + periphery_client(&server)? + .request(api::container::GetContainerLogSearch { + name, + terms, + combinator, + }) + .await + .context("failed at call to periphery") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDeploymentStats { deployment }: GetDeploymentStats, + user: User, + ) -> anyhow::Result { + let Deployment { + name, + config: DeploymentConfig { server_id, .. }, + .. + } = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await?; + if server_id.is_empty() { + return Err(anyhow!("deployment has no server attached")); + } + let server = Server::get_resource(&server_id).await?; + periphery_client(&server)? + .request(api::container::GetContainerStats { name }) + .await + .context("failed to get stats from periphery") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDeploymentActionState { deployment }: GetDeploymentActionState, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &deployment, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = action_states() + .deployment + .get(&deployment.id) + .await + .unwrap_or_default() + .get()?; + Ok(action_state) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDeploymentsSummary {}: GetDeploymentsSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Deployment, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + + let deployments = + find_collect(&db_client().await.deployments, query, None) + .await + .context("failed to count all deployment documents")?; + let mut res = GetDeploymentsSummaryResponse::default(); + let status_cache = deployment_status_cache(); + for deployment in deployments { + res.total += 1; + let status = + status_cache.get(&deployment.id).await.unwrap_or_default(); + match status.curr.state { + DockerContainerState::Running => { + res.running += 1; + } + DockerContainerState::Unknown => { + res.unknown += 1; + } + DockerContainerState::NotDeployed => { + res.not_deployed += 1; + } + _ => { + res.stopped += 1; + } + } + } + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListCommonExtraArgs { query }: ListCommonExtraArgs, + user: User, + ) -> anyhow::Result { + let deployments = + Deployment::list_resources_for_user(query, &user) + .await + .context("failed to get resources matching query")?; + + // first collect with guaranteed uniqueness + let mut res = HashSet::::new(); + + for deployment in deployments { + for extra_arg in deployment.config.extra_args { + res.insert(extra_arg); + } + } + + Ok(res.into_iter().collect()) + } +} diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs new file mode 100644 index 000000000..7d31ba31f --- /dev/null +++ b/bin/core/src/api/read/mod.rs @@ -0,0 +1,215 @@ +use std::time::Instant; + +use anyhow::anyhow; +use async_trait::async_trait; +use axum::{middleware, routing::post, Extension, Router}; +use axum_extra::{headers::ContentType, TypedHeader}; +use monitor_client::{api::read::*, entities::user::User}; +use resolver_api::{ + derive::Resolver, Resolve, ResolveToString, Resolver, +}; +use serde::{Deserialize, Serialize}; +use serror::Json; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{auth::auth_request, config::core_config, state::State}; + +mod alert; +mod alerter; +mod build; +mod builder; +mod deployment; +mod permission; +mod procedure; +mod repo; +mod search; +mod server; +mod server_template; +mod tag; +mod toml; +mod update; +mod user; +mod user_group; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[resolver_target(State)] +#[resolver_args(User)] +#[serde(tag = "type", content = "params")] +enum ReadRequest { + GetVersion(GetVersion), + GetCoreInfo(GetCoreInfo), + + // ==== USER ==== + ListUsers(ListUsers), + GetUsername(GetUsername), + ListApiKeys(ListApiKeys), + ListApiKeysForServiceUser(ListApiKeysForServiceUser), + ListPermissions(ListPermissions), + GetPermissionLevel(GetPermissionLevel), + ListUserTargetPermissions(ListUserTargetPermissions), + + // ==== USER GROUP ==== + GetUserGroup(GetUserGroup), + ListUserGroups(ListUserGroups), + + // ==== SEARCH ==== + FindResources(FindResources), + + // ==== PROCEDURE ==== + GetProceduresSummary(GetProceduresSummary), + GetProcedure(GetProcedure), + GetProcedureActionState(GetProcedureActionState), + ListProcedures(ListProcedures), + + // ==== SERVER TEMPLATE ==== + GetServerTemplate(GetServerTemplate), + ListServerTemplates(ListServerTemplates), + GetServerTemplateSummary(GetServerTemplatesSummary), + + // ==== SERVER ==== + GetServersSummary(GetServersSummary), + GetServer(GetServer), + ListServers(ListServers), + GetServerStatus(GetServerStatus), + GetPeripheryVersion(GetPeripheryVersion), + GetDockerContainers(GetDockerContainers), + GetDockerImages(GetDockerImages), + GetDockerNetworks(GetDockerNetworks), + GetServerActionState(GetServerActionState), + GetHistoricalServerStats(GetHistoricalServerStats), + GetAvailableAccounts(GetAvailableAccounts), + GetAvailableSecrets(GetAvailableSecrets), + + // ==== DEPLOYMENT ==== + GetDeploymentsSummary(GetDeploymentsSummary), + GetDeployment(GetDeployment), + ListDeployments(ListDeployments), + GetDeploymentContainer(GetDeploymentContainer), + GetDeploymentActionState(GetDeploymentActionState), + GetDeploymentStats(GetDeploymentStats), + GetLog(GetLog), + SearchLog(SearchLog), + ListCommonExtraArgs(ListCommonExtraArgs), + + // ==== BUILD ==== + GetBuildsSummary(GetBuildsSummary), + GetBuild(GetBuild), + ListBuilds(ListBuilds), + GetBuildActionState(GetBuildActionState), + GetBuildMonthlyStats(GetBuildMonthlyStats), + GetBuildVersions(GetBuildVersions), + #[to_string_resolver] + ListDockerOrganizations(ListDockerOrganizations), + + // ==== REPO ==== + GetReposSummary(GetReposSummary), + GetRepo(GetRepo), + ListRepos(ListRepos), + GetRepoActionState(GetRepoActionState), + + // ==== BUILDER ==== + GetBuildersSummary(GetBuildersSummary), + GetBuilder(GetBuilder), + ListBuilders(ListBuilders), + GetBuilderAvailableAccounts(GetBuilderAvailableAccounts), + + // ==== ALERTER ==== + GetAlertersSummary(GetAlertersSummary), + GetAlerter(GetAlerter), + ListAlerters(ListAlerters), + + // ==== TOML ==== + ExportAllResourcesToToml(ExportAllResourcesToToml), + ExportResourcesToToml(ExportResourcesToToml), + + // ==== TAG ==== + GetTag(GetTag), + ListTags(ListTags), + + // ==== UPDATE ==== + GetUpdate(GetUpdate), + ListUpdates(ListUpdates), + + // ==== ALERT ==== + ListAlerts(ListAlerts), + GetAlert(GetAlert), + + // ==== SERVER STATS ==== + #[to_string_resolver] + GetSystemInformation(GetSystemInformation), + #[to_string_resolver] + GetSystemStats(GetSystemStats), + #[to_string_resolver] + GetSystemProcesses(GetSystemProcesses), +} + +pub fn router() -> Router { + Router::new() + .route("/", post(handler)) + .layer(middleware::from_fn(auth_request)) +} + +#[instrument(name = "ReadHandler", level = "debug", skip(user))] +async fn handler( + Extension(user): Extension, + Json(request): Json, +) -> serror::Result<(TypedHeader, String)> { + let timer = Instant::now(); + let req_id = Uuid::new_v4(); + debug!( + "/read request {req_id} | user: {} ({})", + user.username, user.id + ); + let res = + State + .resolve_request(request, user) + .await + .map_err(|e| match e { + resolver_api::Error::Serialization(e) => { + anyhow!("{e:?}").context("response serialization error") + } + resolver_api::Error::Inner(e) => e, + }); + if let Err(e) = &res { + warn!("/read request {req_id} error: {e:#}"); + } + let elapsed = timer.elapsed(); + debug!("/read request {req_id} | resolve time: {elapsed:?}"); + Ok((TypedHeader(ContentType::json()), res?)) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetVersion", level = "debug", skip(self))] + async fn resolve( + &self, + GetVersion {}: GetVersion, + _: User, + ) -> anyhow::Result { + Ok(GetVersionResponse { + version: env!("CARGO_PKG_VERSION").to_string(), + }) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetCoreInfo", level = "debug", skip(self))] + async fn resolve( + &self, + GetCoreInfo {}: GetCoreInfo, + _: User, + ) -> anyhow::Result { + let config = core_config(); + Ok(GetCoreInfoResponse { + title: config.title.clone(), + monitoring_interval: config.monitoring_interval, + github_webhook_base_url: config + .github_webhook_base_url + .clone() + .unwrap_or_else(|| config.host.clone()), + }) + } +} diff --git a/bin/core/src/api/read/permission.rs b/bin/core/src/api/read/permission.rs new file mode 100644 index 000000000..74305213f --- /dev/null +++ b/bin/core/src/api/read/permission.rs @@ -0,0 +1,76 @@ +use anyhow::{anyhow, Context}; +use axum::async_trait; +use monitor_client::{ + api::read::{ + GetPermissionLevel, GetPermissionLevelResponse, ListPermissions, + ListPermissionsResponse, ListUserTargetPermissions, + ListUserTargetPermissionsResponse, + }, + entities::{permission::PermissionLevel, user::User}, +}; +use mungos::{find::find_collect, mongodb::bson::doc}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::get_user_permission_on_resource, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListPermissions {}: ListPermissions, + user: User, + ) -> anyhow::Result { + find_collect( + &db_client().await.permissions, + doc! { + "user_target.type": "User", + "user_target.id": &user.id + }, + None, + ) + .await + .context("failed to query db for permissions") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetPermissionLevel { target }: GetPermissionLevel, + user: User, + ) -> anyhow::Result { + if user.admin { + return Ok(PermissionLevel::Write); + } + let (variant, id) = target.extract_variant_id(); + get_user_permission_on_resource(&user.id, variant, id).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUserTargetPermissions { user_target }: ListUserTargetPermissions, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("this method is admin only")); + } + let (variant, id) = user_target.extract_variant_id(); + find_collect( + &db_client().await.permissions, + doc! { + "user_target.type": variant.as_ref(), + "user_target.id": id + }, + None, + ) + .await + .context("failed to query db for permissions") + } +} diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs new file mode 100644 index 000000000..2e690c039 --- /dev/null +++ b/bin/core/src/api/read/procedure.rs @@ -0,0 +1,111 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetProcedure, GetProcedureActionState, + GetProcedureActionStateResponse, GetProcedureResponse, + GetProceduresSummary, GetProceduresSummaryResponse, + ListProcedures, ListProceduresResponse, + }, + entities::{ + permission::PermissionLevel, procedure::Procedure, + update::ResourceTargetVariant, user::User, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProcedure { procedure }: GetProcedure, + user: User, + ) -> anyhow::Result { + Procedure::get_resource_check_permissions( + &procedure, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListProcedures { query }: ListProcedures, + user: User, + ) -> anyhow::Result { + Procedure::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProceduresSummary {}: GetProceduresSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Procedure, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .procedures + .count_documents(query, None) + .await + .context("failed to count all procedure documents")?; + let res = GetProceduresSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProcedureActionState { procedure }: GetProcedureActionState, + user: User, + ) -> anyhow::Result { + let procedure = Procedure::get_resource_check_permissions( + &procedure, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = action_states() + .procedure + .get(&procedure.id) + .await + .unwrap_or_default() + .get()?; + Ok(action_state) + } +} diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs new file mode 100644 index 000000000..e2528963e --- /dev/null +++ b/bin/core/src/api/read/repo.rs @@ -0,0 +1,108 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::*, + entities::{ + permission::PermissionLevel, + repo::{Repo, RepoActionState, RepoListItem}, + update::ResourceTargetVariant, + user::User, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetRepo { repo }: GetRepo, + user: User, + ) -> anyhow::Result { + Repo::get_resource_check_permissions( + &repo, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListRepos { query }: ListRepos, + user: User, + ) -> anyhow::Result> { + Repo::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetRepoActionState { repo }: GetRepoActionState, + user: User, + ) -> anyhow::Result { + let repo = Repo::get_resource_check_permissions( + &repo, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = action_states() + .repo + .get(&repo.id) + .await + .unwrap_or_default() + .get()?; + Ok(action_state) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetReposSummary {}: GetReposSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::Alerter, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .repos + .count_documents(query, None) + .await + .context("failed to count all build documents")?; + let res = GetReposSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} diff --git a/bin/core/src/api/read/search.rs b/bin/core/src/api/read/search.rs new file mode 100644 index 000000000..9c84a025f --- /dev/null +++ b/bin/core/src/api/read/search.rs @@ -0,0 +1,79 @@ +use async_trait::async_trait; +use monitor_client::{ + api::read::{FindResources, FindResourcesResponse}, + entities::{ + build, deployment, procedure, repo, server, + update::ResourceTargetVariant::{self, *}, + user::User, + }, +}; +use resolver_api::Resolve; + +use crate::{helpers::resource::StateResource, state::State}; + +const FIND_RESOURCE_TYPES: [ResourceTargetVariant; 5] = + [Server, Build, Deployment, Repo, Procedure]; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + FindResources { query, resources }: FindResources, + user: User, + ) -> anyhow::Result { + let mut res = FindResourcesResponse::default(); + let resource_types = if resources.is_empty() { + FIND_RESOURCE_TYPES.to_vec() + } else { + resources + .into_iter() + .filter(|r| !matches!(r, System | Builder | Alerter)) + .collect() + }; + for resource_type in resource_types { + match resource_type { + Server => { + res.servers = + server::Server::query_resource_list_items_for_user( + query.clone(), + &user, + ) + .await?; + } + Deployment => { + res.deployments = + deployment::Deployment::query_resource_list_items_for_user( + query.clone(), + &user, + ) + .await?; + } + Build => { + res.builds = + build::Build::query_resource_list_items_for_user( + query.clone(), + &user, + ) + .await?; + } + Repo => { + res.repos = repo::Repo::query_resource_list_items_for_user( + query.clone(), + &user, + ) + .await?; + } + Procedure => { + res.procedures = + procedure::Procedure::query_resource_list_items_for_user( + query.clone(), + &user, + ) + .await?; + } + _ => {} + } + } + Ok(res) + } +} diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs new file mode 100644 index 000000000..93a69b32e --- /dev/null +++ b/bin/core/src/api/read/server.rs @@ -0,0 +1,432 @@ +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, +}; + +use anyhow::{anyhow, Context}; +use async_timing_util::{ + get_timelength_in_ms, unix_timestamp_ms, FIFTEEN_SECONDS_MS, +}; +use async_trait::async_trait; +use monitor_client::{ + api::read::*, + entities::{ + deployment::ContainerSummary, + permission::PermissionLevel, + server::{ + docker_image::ImageSummary, docker_network::DockerNetwork, + Server, ServerActionState, ServerListItem, ServerStatus, + }, + user::User, + }, +}; +use mungos::{ + find::find_collect, + mongodb::{bson::doc, options::FindOptions}, +}; +use periphery_client::api::{self, GetAccountsResponse}; +use resolver_api::{Resolve, ResolveToString}; +use tokio::sync::Mutex; + +use crate::{ + helpers::{periphery_client, resource::StateResource}, + state::{action_states, db_client, server_status_cache, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetServersSummary {}: GetServersSummary, + user: User, + ) -> anyhow::Result { + let servers = Server::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await?; + let mut res = GetServersSummaryResponse::default(); + for server in servers { + res.total += 1; + match server.info.status { + ServerStatus::Ok => { + res.healthy += 1; + } + ServerStatus::NotOk => { + res.unhealthy += 1; + } + ServerStatus::Disabled => { + res.disabled += 1; + } + } + } + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + req: GetPeripheryVersion, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &req.server, + &user, + PermissionLevel::Read, + ) + .await?; + let version = server_status_cache() + .get(&server.id) + .await + .map(|s| s.version.clone()) + .unwrap_or(String::from("unknown")); + Ok(GetPeripheryVersionResponse { version }) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + req: GetServer, + user: User, + ) -> anyhow::Result { + Server::get_resource_check_permissions( + &req.server, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListServers { query }: ListServers, + user: User, + ) -> anyhow::Result> { + Server::list_resource_list_items_for_user(query, &user).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetServerStatus { server }: GetServerStatus, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let status = server_status_cache() + .get(&server.id) + .await + .ok_or(anyhow!("did not find cached status for server"))?; + let response = GetServerStatusResponse { + status: status.status, + }; + Ok(response) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetServerActionState { server }: GetServerActionState, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = action_states() + .server + .get(&server.id) + .await + .unwrap_or_default() + .get()?; + Ok(action_state) + } +} + +// This protects the peripheries from spam requests +const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS; +type SystemInfoCache = Mutex>>; +fn system_info_cache() -> &'static SystemInfoCache { + static SYSTEM_INFO_CACHE: OnceLock = + OnceLock::new(); + SYSTEM_INFO_CACHE.get_or_init(Default::default) +} + +#[async_trait] +impl ResolveToString for State { + async fn resolve_to_string( + &self, + GetSystemInformation { server }: GetSystemInformation, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + + let mut lock = system_info_cache().lock().await; + let res = match lock.get(&server.id) { + Some(cached) if cached.1 > unix_timestamp_ms() => { + cached.0.clone() + } + _ => { + let stats = periphery_client(&server)? + .request(api::stats::GetSystemInformation {}) + .await?; + let res = serde_json::to_string(&stats)?; + lock.insert( + server.id, + (res.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY) + .into(), + ); + res + } + }; + Ok(res) + } +} + +#[async_trait] +impl ResolveToString for State { + async fn resolve_to_string( + &self, + GetSystemStats { server }: GetSystemStats, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let status = + server_status_cache().get(&server.id).await.with_context( + || format!("did not find status for server at {}", server.id), + )?; + let stats = status + .stats + .as_ref() + .context("server stats not available")?; + let stats = serde_json::to_string(&stats)?; + Ok(stats) + } +} + +// This protects the peripheries from spam requests +const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS; +type ProcessesCache = Mutex>>; +fn processes_cache() -> &'static ProcessesCache { + static PROCESSES_CACHE: OnceLock = OnceLock::new(); + PROCESSES_CACHE.get_or_init(Default::default) +} + +#[async_trait] +impl ResolveToString for State { + async fn resolve_to_string( + &self, + GetSystemProcesses { server }: GetSystemProcesses, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let mut lock = processes_cache().lock().await; + let res = match lock.get(&server.id) { + Some(cached) if cached.1 > unix_timestamp_ms() => { + cached.0.clone() + } + _ => { + let stats = periphery_client(&server)? + .request(api::stats::GetSystemProcesses {}) + .await?; + let res = serde_json::to_string(&stats)?; + lock.insert( + server.id, + (res.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY) + .into(), + ); + res + } + }; + Ok(res) + } +} + +const STATS_PER_PAGE: i64 = 500; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetHistoricalServerStats { + server, + granularity, + page, + }: GetHistoricalServerStats, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let granularity = + get_timelength_in_ms(granularity.to_string().parse().unwrap()) + as i64; + let mut ts_vec = Vec::::new(); + let curr_ts = unix_timestamp_ms() as i64; + let mut curr_ts = curr_ts + - curr_ts % granularity + - granularity * STATS_PER_PAGE * page as i64; + for _ in 0..STATS_PER_PAGE { + ts_vec.push(curr_ts); + curr_ts -= granularity; + } + + let stats = find_collect( + &db_client().await.stats, + doc! { + "sid": server.id, + "ts": { "$in": ts_vec }, + }, + FindOptions::builder() + .sort(doc! { "ts": -1 }) + .skip(page as u64 * STATS_PER_PAGE as u64) + .limit(STATS_PER_PAGE) + .build(), + ) + .await + .context("failed to pull stats from db")?; + let next_page = if stats.len() == STATS_PER_PAGE as usize { + Some(page + 1) + } else { + None + }; + let res = GetHistoricalServerStatsResponse { stats, next_page }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDockerImages { server }: GetDockerImages, + user: User, + ) -> anyhow::Result> { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + periphery_client(&server)? + .request(api::build::GetImageList {}) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDockerNetworks { server }: GetDockerNetworks, + user: User, + ) -> anyhow::Result> { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + periphery_client(&server)? + .request(api::network::GetNetworkList {}) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetDockerContainers { server }: GetDockerContainers, + user: User, + ) -> anyhow::Result> { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + periphery_client(&server)? + .request(api::container::GetContainerList {}) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAvailableAccounts { server }: GetAvailableAccounts, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let GetAccountsResponse { github, docker } = + periphery_client(&server)? + .request(api::GetAccounts {}) + .await + .context("failed to get accounts from periphery")?; + let res = GetAvailableAccountsResponse { github, docker }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAvailableSecrets { server }: GetAvailableSecrets, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Read, + ) + .await?; + let secrets = periphery_client(&server)? + .request(api::GetSecrets {}) + .await + .context("failed to get accounts from periphery")?; + Ok(secrets) + } +} diff --git a/bin/core/src/api/read/server_template.rs b/bin/core/src/api/read/server_template.rs new file mode 100644 index 000000000..7d270838b --- /dev/null +++ b/bin/core/src/api/read/server_template.rs @@ -0,0 +1,88 @@ +use std::str::FromStr; + +use anyhow::Context; +use axum::async_trait; +use monitor_client::{ + api::read::{ + GetServerTemplate, GetServerTemplateResponse, + GetServerTemplatesSummary, GetServerTemplatesSummaryResponse, + ListServerTemplates, ListServerTemplatesResponse, + }, + entities::{ + permission::PermissionLevel, server_template::ServerTemplate, + update::ResourceTargetVariant, user::User, + }, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::{ + get_resource_ids_for_non_admin, StateResource, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetServerTemplate { server_template }: GetServerTemplate, + user: User, + ) -> anyhow::Result { + ServerTemplate::get_resource_check_permissions( + &server_template, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListServerTemplates { query }: ListServerTemplates, + user: User, + ) -> anyhow::Result { + ServerTemplate::list_resource_list_items_for_user(query, &user) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetServerTemplatesSummary {}: GetServerTemplatesSummary, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + None + } else { + let ids = get_resource_ids_for_non_admin( + &user.id, + ResourceTargetVariant::ServerTemplate, + ) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + let query = doc! { + "_id": { "$in": ids } + }; + Some(query) + }; + let total = db_client() + .await + .builders + .count_documents(query, None) + .await + .context("failed to count all builder documents")?; + let res = GetServerTemplatesSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} diff --git a/bin/core/src/api/read/tag.rs b/bin/core/src/api/read/tag.rs new file mode 100644 index 000000000..293ee8224 --- /dev/null +++ b/bin/core/src/api/read/tag.rs @@ -0,0 +1,37 @@ +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{GetTag, ListTags}, + entities::{tag::Tag, user::User}, +}; +use mungos::find::find_collect; +use resolver_api::Resolve; + +use crate::{ + helpers::query::get_tag, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetTag { tag }: GetTag, + _: User, + ) -> anyhow::Result { + get_tag(&tag).await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListTags { query }: ListTags, + _: User, + ) -> anyhow::Result> { + find_collect(&db_client().await.tags, query, None) + .await + .context("failed to get tags from db") + } +} diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs new file mode 100644 index 000000000..398824f0e --- /dev/null +++ b/bin/core/src/api/read/toml.rs @@ -0,0 +1,481 @@ +use std::collections::HashMap; + +use anyhow::Context; +use axum::async_trait; +use monitor_client::{ + api::{ + execute::Execution, + read::{ + ExportAllResourcesToToml, ExportAllResourcesToTomlResponse, + ExportResourcesToToml, ExportResourcesToTomlResponse, + GetUserGroup, ListUserTargetPermissions, + }, + }, + entities::{ + alerter::Alerter, + build::Build, + builder::{Builder, BuilderConfig}, + deployment::{Deployment, DeploymentImage}, + permission::{PermissionLevel, UserTarget}, + procedure::Procedure, + repo::Repo, + resource::Resource, + server::Server, + server_template::ServerTemplate, + toml::{ + PermissionToml, ResourceToml, ResourcesToml, UserGroupToml, + }, + update::ResourceTarget, + user::User, + }, +}; +use mungos::find::find_collect; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + query::get_user_user_group_ids, resource::StateResource, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ExportAllResourcesToToml {}: ExportAllResourcesToToml, + user: User, + ) -> anyhow::Result { + let mut targets = Vec::::new(); + + targets.extend( + Alerter::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Alerter(resource.id)), + ); + targets.extend( + Builder::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Builder(resource.id)), + ); + targets.extend( + Server::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Server(resource.id)), + ); + targets.extend( + Deployment::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Deployment(resource.id)), + ); + targets.extend( + Build::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Build(resource.id)), + ); + targets.extend( + Repo::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Repo(resource.id)), + ); + targets.extend( + Procedure::list_resource_list_items_for_user( + Default::default(), + &user, + ) + .await? + .into_iter() + .map(|resource| ResourceTarget::Procedure(resource.id)), + ); + + let user_groups = if user.admin { + find_collect(&db_client().await.user_groups, None, None) + .await + .context("failed to query db for user groups")? + .into_iter() + .map(|user_group| user_group.id) + .collect() + } else { + get_user_user_group_ids(&user.id).await? + }; + + self + .resolve( + ExportResourcesToToml { + targets, + user_groups, + }, + user, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ExportResourcesToToml { + targets, + user_groups, + }: ExportResourcesToToml, + user: User, + ) -> anyhow::Result { + let mut res = ResourcesToml::default(); + let names = ResourceNames::new() + .await + .context("failed to init resource name maps")?; + for target in targets { + match target { + ResourceTarget::Alerter(id) => { + let alerter = Alerter::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + res.alerters.push(convert_resource(alerter, &names.tags)) + } + ResourceTarget::ServerTemplate(id) => { + let template = + ServerTemplate::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + res + .server_templates + .push(convert_resource(template, &names.tags)) + } + ResourceTarget::Server(id) => { + let server = Server::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + res.servers.push(convert_resource(server, &names.tags)) + } + ResourceTarget::Builder(id) => { + let mut builder = Builder::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + // replace server id of builder + if let BuilderConfig::Server(config) = &mut builder.config { + config.server_id.clone_from( + names.servers.get(&id).unwrap_or(&String::new()), + ) + } + res.builders.push(convert_resource(builder, &names.tags)) + } + ResourceTarget::Build(id) => { + let mut build = Build::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + // replace builder id of build + build.config.builder_id.clone_from( + names + .builders + .get(&build.config.builder_id) + .unwrap_or(&String::new()), + ); + res.builds.push(convert_resource(build, &names.tags)) + } + ResourceTarget::Deployment(id) => { + let mut deployment = + Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + // replace deployment server with name + deployment.config.server_id.clone_from( + names + .servers + .get(&deployment.config.server_id) + .unwrap_or(&String::new()), + ); + // replace deployment build id with name + if let DeploymentImage::Build { build_id, .. } = + &mut deployment.config.image + { + build_id.clone_from( + names.builds.get(build_id).unwrap_or(&String::new()), + ); + } + res + .deployments + .push(convert_resource(deployment, &names.tags)) + } + ResourceTarget::Repo(id) => { + let mut repo = Repo::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + // replace repo server with name + repo.config.server_id.clone_from( + names + .servers + .get(&repo.config.server_id) + .unwrap_or(&String::new()), + ); + res.repos.push(convert_resource(repo, &names.tags)) + } + ResourceTarget::Procedure(id) => { + add_procedure(&id, &mut res, &user, &names) + .await + .with_context(|| { + format!("failed to add procedure {id}") + })?; + } + ResourceTarget::System(_) => continue, + }; + } + + add_user_groups(user_groups, &mut res, &user) + .await + .context("failed to add user groups")?; + + let toml = toml::to_string_pretty(&res) + .context("failed to serialize resources to toml")?; + + Ok(ExportResourcesToTomlResponse { toml }) + } +} + +async fn add_procedure( + id: &str, + res: &mut ResourcesToml, + user: &User, + names: &ResourceNames, +) -> anyhow::Result<()> { + let mut procedure = Procedure::get_resource_check_permissions( + id, + user, + PermissionLevel::Read, + ) + .await?; + for execution in &mut procedure.config.executions { + match &mut execution.execution { + Execution::RunProcedure(exec) => exec.procedure.clone_from( + names + .procedures + .get(&exec.procedure) + .unwrap_or(&String::new()), + ), + Execution::RunBuild(exec) => exec.build.clone_from( + names.builds.get(&exec.build).unwrap_or(&String::new()), + ), + Execution::Deploy(exec) => exec.deployment.clone_from( + names + .deployments + .get(&exec.deployment) + .unwrap_or(&String::new()), + ), + Execution::StartContainer(exec) => exec.deployment.clone_from( + names + .deployments + .get(&exec.deployment) + .unwrap_or(&String::new()), + ), + Execution::StopContainer(exec) => exec.deployment.clone_from( + names + .deployments + .get(&exec.deployment) + .unwrap_or(&String::new()), + ), + Execution::RemoveContainer(exec) => exec.deployment.clone_from( + names + .deployments + .get(&exec.deployment) + .unwrap_or(&String::new()), + ), + Execution::CloneRepo(exec) => exec.repo.clone_from( + names.repos.get(&exec.repo).unwrap_or(&String::new()), + ), + Execution::PullRepo(exec) => exec.repo.clone_from( + names.repos.get(&exec.repo).unwrap_or(&String::new()), + ), + Execution::StopAllContainers(exec) => exec.server.clone_from( + names.servers.get(&exec.server).unwrap_or(&String::new()), + ), + Execution::PruneDockerNetworks(exec) => exec.server.clone_from( + names.servers.get(&exec.server).unwrap_or(&String::new()), + ), + Execution::PruneDockerImages(exec) => exec.server.clone_from( + names.servers.get(&exec.server).unwrap_or(&String::new()), + ), + Execution::PruneDockerContainers(exec) => { + exec.server.clone_from( + names.servers.get(&exec.server).unwrap_or(&String::new()), + ) + } + Execution::None(_) => continue, + } + } + res + .procedures + .push(convert_resource(procedure, &names.tags)); + Ok(()) +} + +struct ResourceNames { + tags: HashMap, + servers: HashMap, + builders: HashMap, + builds: HashMap, + repos: HashMap, + deployments: HashMap, + procedures: HashMap, +} + +impl ResourceNames { + async fn new() -> anyhow::Result { + let db = db_client().await; + Ok(ResourceNames { + tags: find_collect(&db.tags, None, None) + .await + .context("failed to get all tags")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + servers: find_collect(&db.servers, None, None) + .await + .context("failed to get all servers")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + builders: find_collect(&db.builders, None, None) + .await + .context("failed to get all builders")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + builds: find_collect(&db.builds, None, None) + .await + .context("failed to get all builds")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + repos: find_collect(&db.repos, None, None) + .await + .context("failed to get all repos")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + deployments: find_collect(&db.deployments, None, None) + .await + .context("failed to get all deployments")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + procedures: find_collect(&db.procedures, None, None) + .await + .context("failed to get all procedures")? + .into_iter() + .map(|t| (t.id, t.name)) + .collect::>(), + }) + } +} + +async fn add_user_groups( + user_groups: Vec, + res: &mut ResourcesToml, + user: &User, +) -> anyhow::Result<()> { + let db = db_client().await; + + let usernames = find_collect(&db.users, None, None) + .await? + .into_iter() + .map(|user| (user.id, user.username)) + .collect::>(); + + for user_group in user_groups { + let ug = State + .resolve(GetUserGroup { user_group }, user.clone()) + .await?; + // this method is admin only, but we already know user can see user group if above does not return Err + let permissions = State + .resolve( + ListUserTargetPermissions { + user_target: UserTarget::UserGroup(ug.id), + }, + User { + admin: true, + ..Default::default() + }, + ) + .await? + .into_iter() + .map(|permission| PermissionToml { + target: permission.resource_target, + level: permission.level, + }) + .collect(); + res.user_groups.push(UserGroupToml { + name: ug.name, + users: ug + .users + .into_iter() + .filter_map(|user_id| usernames.get(&user_id).cloned()) + .collect(), + permissions, + }); + } + Ok(()) +} + +fn convert_resource( + resource: Resource, + tag_names: &HashMap, +) -> ResourceToml +where + Config: Into, +{ + ResourceToml { + name: resource.name, + tags: resource + .tags + .iter() + .filter_map(|t| tag_names.get(t).cloned()) + .collect(), + description: resource.description, + config: resource.config.into(), + } +} diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs new file mode 100644 index 000000000..8982c6e2d --- /dev/null +++ b/bin/core/src/api/read/update.rs @@ -0,0 +1,214 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::read::{GetUpdate, ListUpdates, ListUpdatesResponse}, + entities::{ + alerter::Alerter, + build::Build, + builder::Builder, + deployment::Deployment, + permission::PermissionLevel, + procedure::Procedure, + repo::Repo, + server::Server, + server_template::ServerTemplate, + update::{ResourceTarget, Update, UpdateListItem}, + user::User, + }, +}; +use mungos::{ + by_id::find_one_by_id, + find::find_collect, + mongodb::{bson::doc, options::FindOptions}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::resource::StateResource, + state::{db_client, State}, +}; + +const UPDATES_PER_PAGE: i64 = 20; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUpdates { query, page }: ListUpdates, + user: User, + ) -> anyhow::Result { + let query = if user.admin { + query + } else { + let server_ids = + Server::get_resource_ids_for_non_admin(&user.id).await?; + let deployment_ids = + Deployment::get_resource_ids_for_non_admin(&user.id).await?; + let build_ids = + Build::get_resource_ids_for_non_admin(&user.id).await?; + let repo_ids = + Repo::get_resource_ids_for_non_admin(&user.id).await?; + let procedure_ids = + Procedure::get_resource_ids_for_non_admin(&user.id).await?; + let builder_ids = + Builder::get_resource_ids_for_non_admin(&user.id).await?; + let alerter_ids = + Alerter::get_resource_ids_for_non_admin(&user.id).await?; + let mut query = query.unwrap_or_default(); + query.extend(doc! { + "$or": [ + { "target.type": "Server", "target.id": { "$in": &server_ids } }, + { "target.type": "Deployment", "target.id": { "$in": &deployment_ids } }, + { "target.type": "Build", "target.id": { "$in": &build_ids } }, + { "target.type": "Repo", "target.id": { "$in": &repo_ids } }, + { "target.type": "Procedure", "target.id": { "$in": &procedure_ids } }, + { "target.type": "Builder", "target.id": { "$in": &builder_ids } }, + { "target.type": "Alerter", "target.id": { "$in": &alerter_ids } }, + ] + }); + query.into() + }; + + let usernames = + find_collect(&db_client().await.users, None, None) + .await + .context("failed to pull users from db")? + .into_iter() + .map(|u| (u.id, u.username)) + .collect::>(); + + let updates = find_collect( + &db_client().await.updates, + query, + FindOptions::builder() + .sort(doc! { "start_ts": -1 }) + .skip(page as u64 * UPDATES_PER_PAGE as u64) + .limit(UPDATES_PER_PAGE) + .build(), + ) + .await + .context("failed to pull updates from db")? + .into_iter() + .map(|u| { + let username = if User::is_service_user(&u.operator) { + u.operator.clone() + } else { + usernames + .get(&u.operator) + .cloned() + .unwrap_or("unknown".to_string()) + }; + UpdateListItem { + username, + id: u.id, + operation: u.operation, + start_ts: u.start_ts, + success: u.success, + operator: u.operator, + target: u.target, + status: u.status, + version: u.version, + } + }) + .collect::>(); + + let next_page = if updates.len() == UPDATES_PER_PAGE as usize { + Some(page + 1) + } else { + None + }; + + Ok(ListUpdatesResponse { updates, next_page }) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetUpdate { id }: GetUpdate, + user: User, + ) -> anyhow::Result { + let update = find_one_by_id(&db_client().await.updates, &id) + .await + .context("failed to query to db")? + .context("no update exists with given id")?; + if user.admin { + return Ok(update); + } + match &update.target { + ResourceTarget::System(_) => { + return Err(anyhow!( + "user must be admin to view system updates" + )) + } + ResourceTarget::Server(id) => { + Server::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Deployment(id) => { + Deployment::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Build(id) => { + Build::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Repo(id) => { + Repo::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Builder(id) => { + Builder::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Alerter(id) => { + Alerter::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::Procedure(id) => { + Procedure::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + ResourceTarget::ServerTemplate(id) => { + ServerTemplate::get_resource_check_permissions( + id, + &user, + PermissionLevel::Read, + ) + .await?; + } + } + Ok(update) + } +} diff --git a/bin/core/src/api/read/user.rs b/bin/core/src/api/read/user.rs new file mode 100644 index 000000000..8c7886f11 --- /dev/null +++ b/bin/core/src/api/read/user.rs @@ -0,0 +1,111 @@ +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetUsername, GetUsernameResponse, ListApiKeys, + ListApiKeysForServiceUser, ListApiKeysForServiceUserResponse, + ListApiKeysResponse, ListUsers, ListUsersResponse, + }, + entities::user::{User, UserConfig}, +}; +use mungos::{ + by_id::find_one_by_id, find::find_collect, mongodb::bson::doc, +}; +use resolver_api::Resolve; + +use crate::state::{db_client, State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetUsername { user_id }: GetUsername, + _: User, + ) -> anyhow::Result { + let user = find_one_by_id(&db_client().await.users, &user_id) + .await + .context("failed at mongo query for user")? + .context("no user found with id")?; + + Ok(GetUsernameResponse { + username: user.username, + }) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUsers {}: ListUsers, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("this route is only accessable by admins")); + } + let mut users = + find_collect(&db_client().await.users, None, None) + .await + .context("failed to pull users from db")?; + users.iter_mut().for_each(|user| user.sanitize()); + Ok(users) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListApiKeys {}: ListApiKeys, + user: User, + ) -> anyhow::Result { + let api_keys = find_collect( + &db_client().await.api_keys, + doc! { "user_id": &user.id }, + None, + ) + .await + .context("failed to query db for api keys")? + .into_iter() + .map(|mut api_keys| { + api_keys.sanitize(); + api_keys + }) + .collect(); + Ok(api_keys) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListApiKeysForServiceUser { user_id }: ListApiKeysForServiceUser, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This method is admin only.")); + } + let user = find_one_by_id(&db_client().await.users, &user_id) + .await + .context("failed to query db for users")? + .context("user at id not found")?; + let UserConfig::Service { .. } = user.config else { + return Err(anyhow!("Given user is not service user")); + }; + let api_keys = find_collect( + &db_client().await.api_keys, + doc! { "user_id": user_id }, + None, + ) + .await + .context("failed to query db for api keys")? + .into_iter() + .map(|mut api_keys| { + api_keys.sanitize(); + api_keys + }) + .collect(); + Ok(api_keys) + } +} diff --git a/bin/core/src/api/read/user_group.rs b/bin/core/src/api/read/user_group.rs new file mode 100644 index 000000000..b3103de3a --- /dev/null +++ b/bin/core/src/api/read/user_group.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetUserGroup, GetUserGroupResponse, ListUserGroups, + ListUserGroupsResponse, + }, + entities::user::User, +}; +use mungos::{ + find::find_collect, + mongodb::bson::{doc, oid::ObjectId, Document}, +}; +use resolver_api::Resolve; + +use crate::state::{db_client, State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetUserGroup { user_group }: GetUserGroup, + user: User, + ) -> anyhow::Result { + let mut filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + // Don't allow non admin users to get UserGroups they aren't a part of. + if !user.admin { + // Filter for only UserGroups which contain the users id + filter.insert("users", &user.id); + } + db_client() + .await + .user_groups + .find_one(filter, None) + .await + .context("failed to query db for user groups")? + .context("no UserGroup found with given name or id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUserGroups {}: ListUserGroups, + user: User, + ) -> anyhow::Result { + let mut filter = Document::new(); + if !user.admin { + filter.insert("users", &user.id); + } + find_collect(&db_client().await.user_groups, filter, None) + .await + .context("failed to query db for UserGroups") + } +} diff --git a/bin/core/src/api/write/alerter.rs b/bin/core/src/api/write/alerter.rs new file mode 100644 index 000000000..291bb7ebd --- /dev/null +++ b/bin/core/src/api/write/alerter.rs @@ -0,0 +1,240 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::{ + CopyAlerter, CreateAlerter, DeleteAlerter, UpdateAlerter, + }, + entities::{ + alerter::{Alerter, AlerterInfo}, + monitor_timestamp, + permission::PermissionLevel, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update}, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateAlerter", skip(self, user))] + async fn resolve( + &self, + CreateAlerter { name, config }: CreateAlerter, + user: User, + ) -> anyhow::Result { + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + let start_ts = monitor_timestamp(); + let is_default = db_client() + .await + .alerters + .find_one(None, None) + .await? + .is_none(); + let alerter = Alerter { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: AlerterInfo { is_default }, + }; + let alerter_id = db_client() + .await + .alerters + .insert_one(alerter, None) + .await + .context("failed to add alerter to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let alerter = Alerter::get_resource(&alerter_id).await?; + + create_permission(&user, &alerter, PermissionLevel::Write).await; + + let mut update = + make_update(&alerter, Operation::CreateAlerter, &user); + + update.push_simple_log( + "create alerter", + format!( + "created alerter\nid: {}\nname: {}", + alerter.id, alerter.name + ), + ); + update + .push_simple_log("config", format!("{:#?}", alerter.config)); + + update.finalize(); + + add_update(update).await?; + + Ok(alerter) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyAlerter", skip(self, user))] + async fn resolve( + &self, + CopyAlerter { name, id }: CopyAlerter, + user: User, + ) -> anyhow::Result { + let Alerter { + config, + description, + .. + } = Alerter::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let start_ts = monitor_timestamp(); + let alerter = Alerter { + id: Default::default(), + name, + updated_at: start_ts, + description, + config, + tags: Default::default(), + info: Default::default(), + }; + let alerter_id = db_client() + .await + .alerters + .insert_one(alerter, None) + .await + .context("failed to add alerter to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let alerter = Alerter::get_resource(&alerter_id).await?; + + create_permission(&user, &alerter, PermissionLevel::Write).await; + + let mut update = + make_update(&alerter, Operation::CreateAlerter, &user); + + update.push_simple_log( + "create alerter", + format!( + "created alerter\nid: {}\nname: {}", + alerter.id, alerter.name + ), + ); + + update + .push_simple_log("config", format!("{:#?}", alerter.config)); + + update.finalize(); + + add_update(update).await?; + + Ok(alerter) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteAlerter", skip(self, user))] + async fn resolve( + &self, + DeleteAlerter { id }: DeleteAlerter, + user: User, + ) -> anyhow::Result { + let alerter = Alerter::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = + make_update(&alerter, Operation::DeleteAlerter, &user); + + delete_one_by_id(&db_client().await.alerters, &id, None) + .await + .context("failed to delete alerter from database")?; + + delete_all_permissions_on_resource(&alerter).await; + + update.push_simple_log( + "delete alerter", + format!("deleted alerter {}", alerter.name), + ); + + update.finalize(); + + add_update(update).await?; + + remove_from_recently_viewed(&alerter).await?; + + Ok(alerter) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateAlerter", skip(self, user))] + async fn resolve( + &self, + UpdateAlerter { id, config }: UpdateAlerter, + user: User, + ) -> anyhow::Result { + let alerter = Alerter::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = + make_update(&alerter, Operation::UpdateAlerter, &user); + + update.push_simple_log( + "alerter update", + serde_json::to_string_pretty(&config)?, + ); + + let config = alerter.config.merge_partial(config); + + update_one_by_id( + &db_client().await.alerters, + &id, + mungos::update::Update::FlattenSet( + doc! { "config": to_bson(&config)? }, + ), + None, + ) + .await + .with_context(|| format!("failed to update alerter {id}"))?; + + let alerter = Alerter::get_resource(&id).await?; + + update.finalize(); + add_update(update).await?; + + Ok(alerter) + } +} diff --git a/bin/core/src/api/write/api_key.rs b/bin/core/src/api/write/api_key.rs new file mode 100644 index 000000000..bd9eb4420 --- /dev/null +++ b/bin/core/src/api/write/api_key.rs @@ -0,0 +1,152 @@ +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + api_key::ApiKey, + monitor_timestamp, + user::{User, UserConfig}, + }, +}; +use mungos::{by_id::find_one_by_id, mongodb::bson::doc}; +use resolver_api::Resolve; + +use crate::{ + auth::random_string, + helpers::query::get_user, + state::{db_client, State}, +}; + +const SECRET_LENGTH: usize = 40; +const BCRYPT_COST: u32 = 10; + +#[async_trait] +impl Resolve for State { + #[instrument( + name = "CreateApiKey", + level = "debug", + skip(self, user) + )] + async fn resolve( + &self, + CreateApiKey { name, expires }: CreateApiKey, + user: User, + ) -> anyhow::Result { + let user = get_user(&user.id).await?; + + let key = format!("K-{}", random_string(SECRET_LENGTH)); + let secret = format!("S-{}", random_string(SECRET_LENGTH)); + let secret_hash = bcrypt::hash(&secret, BCRYPT_COST) + .context("failed at hashing secret string")?; + + let api_key = ApiKey { + name, + key: key.clone(), + secret: secret_hash, + user_id: user.id.clone(), + created_at: monitor_timestamp(), + expires, + }; + db_client() + .await + .api_keys + .insert_one(api_key, None) + .await + .context("failed to create api key on db")?; + Ok(CreateApiKeyResponse { key, secret }) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument( + name = "DeleteApiKey", + level = "debug", + skip(self, user) + )] + async fn resolve( + &self, + DeleteApiKey { key }: DeleteApiKey, + user: User, + ) -> anyhow::Result { + let client = db_client().await; + let key = client + .api_keys + .find_one(doc! { "key": &key }, None) + .await + .context("failed at db query")? + .context("no api key with key found")?; + if user.id != key.user_id { + return Err(anyhow!("api key does not belong to user")); + } + client + .api_keys + .delete_one(doc! { "key": key.key }, None) + .await + .context("failed to delete api key from db")?; + Ok(DeleteApiKeyResponse {}) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateApiKeyForServiceUser", skip(self, user))] + async fn resolve( + &self, + CreateApiKeyForServiceUser { + user_id, + name, + expires, + }: CreateApiKeyForServiceUser, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("user not admin")); + } + let service_user = + find_one_by_id(&db_client().await.users, &user_id) + .await + .context("failed to query db for user")? + .context("no user found with id")?; + let UserConfig::Service { .. } = &service_user.config else { + return Err(anyhow!("user is not service user")); + }; + self + .resolve(CreateApiKey { name, expires }, service_user) + .await + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteApiKeyForServiceUser", skip(self, user))] + async fn resolve( + &self, + DeleteApiKeyForServiceUser { key }: DeleteApiKeyForServiceUser, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("user not admin")); + } + let db = db_client().await; + let api_key = db + .api_keys + .find_one(doc! { "key": &key }, None) + .await + .context("failed to query db for api key")? + .context("did not find matching api key")?; + let service_user = + find_one_by_id(&db_client().await.users, &api_key.user_id) + .await + .context("failed to query db for user")? + .context("no user found with id")?; + let UserConfig::Service { .. } = &service_user.config else { + return Err(anyhow!("user is not service user")); + }; + db.api_keys + .delete_one(doc! { "key": key }, None) + .await + .context("failed to delete api key on db")?; + Ok(DeleteApiKeyForServiceUserResponse {}) + } +} diff --git a/bin/core/src/api/write/build.rs b/bin/core/src/api/write/build.rs new file mode 100644 index 000000000..25fb7059d --- /dev/null +++ b/bin/core/src/api/write/build.rs @@ -0,0 +1,309 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + build::Build, + builder::Builder, + monitor_timestamp, + permission::PermissionLevel, + to_monitor_name, + update::{Log, UpdateStatus}, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::update_one_by_id, + mongodb::bson::{doc, oid::ObjectId, to_document}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, empty_or_only_spaces, + remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateBuild", skip(self, user))] + async fn resolve( + &self, + CreateBuild { name, mut config }: CreateBuild, + user: User, + ) -> anyhow::Result { + if !user.admin && !user.create_build_permissions { + return Err(anyhow!( + "User does not have create build permissions." + )); + } + let name = to_monitor_name(&name); + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + if let Some(builder_id) = &config.builder_id { + let builder = Builder::get_resource_check_permissions(builder_id, &user, PermissionLevel::Read) + .await + .context("cannot create build using this builder. user must have at least read permissions on the builder.")?; + config.builder_id = Some(builder.id) + } + let start_ts = monitor_timestamp(); + let build = Build { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: Default::default(), + }; + let build_id = db_client() + .await + .builds + .insert_one(build, None) + .await + .context("failed to add build to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let build = Build::get_resource(&build_id).await?; + + create_permission(&user, &build, PermissionLevel::Write).await; + + let mut update = + make_update(&build, Operation::CreateBuild, &user); + + update.push_simple_log( + "create build", + format!( + "created build\nid: {}\nname: {}", + build.id, build.name + ), + ); + + update.push_simple_log("config", format!("{:#?}", build.config)); + + update.finalize(); + + add_update(update).await?; + + Ok(build) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyBuild", skip(self, user))] + async fn resolve( + &self, + CopyBuild { name, id }: CopyBuild, + user: User, + ) -> anyhow::Result { + if !user.admin && !user.create_build_permissions { + return Err(anyhow!( + "User does not have create build permissions." + )); + } + let name = to_monitor_name(&name); + let Build { + config, + description, + tags, + .. + } = Build::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Builder::get_resource_check_permissions(&config.builder_id, &user, PermissionLevel::Read) + .await + .context("cannot create build using this builder. user must have at least read permissions on the builder.")?; + let start_ts = monitor_timestamp(); + let build = Build { + id: Default::default(), + name, + updated_at: start_ts, + description, + tags, + config, + info: Default::default(), + }; + let build_id = db_client() + .await + .builds + .insert_one(build, None) + .await + .context("failed to add build to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let build = Build::get_resource(&build_id).await?; + + create_permission(&user, &build, PermissionLevel::Write).await; + + let mut update = + make_update(&build, Operation::CreateBuild, &user); + + update.push_simple_log( + "create build", + format!( + "created build\nid: {}\nname: {}", + build.id, build.name + ), + ); + update.push_simple_log( + "config", + serde_json::to_string_pretty(&build)?, + ); + + update.finalize(); + + add_update(update).await?; + + Ok(build) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteBuild", skip(self, user))] + async fn resolve( + &self, + DeleteBuild { id }: DeleteBuild, + user: User, + ) -> anyhow::Result { + if action_states() + .build + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("build busy")); + } + + let build = Build::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = + make_update(&build, Operation::DeleteBuild, &user); + update.status = UpdateStatus::InProgress; + update.id = add_update(update.clone()).await?; + + let res = db_client() + .await + .builds + .delete_one(doc! { "_id": ObjectId::from_str(&id)? }, None) + .await + .context("failed to delete build from database"); + + delete_all_permissions_on_resource(&build).await; + + let log = match res { + Ok(_) => Log::simple( + "delete build", + format!("deleted build {}", build.name), + ), + Err(e) => Log::error( + "delete build", + format!("failed to delete build\n{e:#?}"), + ), + }; + + update.logs.push(log); + update.finalize(); + update_update(update).await?; + + remove_from_recently_viewed(&build).await?; + + Ok(build) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateBuild", skip(self, user))] + async fn resolve( + &self, + UpdateBuild { id, mut config }: UpdateBuild, + user: User, + ) -> anyhow::Result { + if action_states() + .build + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("build busy")); + } + + let build = Build::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + if let Some(builder_id) = &config.builder_id { + let builder = Builder::get_resource_check_permissions(builder_id, &user, PermissionLevel::Read) + .await + .context("cannot create build using this builder. user must have at least read permissions on the builder.")?; + config.builder_id = Some(builder.id) + } + + if let Some(build_args) = &mut config.build_args { + build_args.retain(|v| { + !empty_or_only_spaces(&v.variable) + && !empty_or_only_spaces(&v.value) + }) + } + if let Some(extra_args) = &mut config.extra_args { + extra_args.retain(|v| !empty_or_only_spaces(v)) + } + + let config_doc = to_document(&config) + .context("failed to serialize config to bson document")?; + + update_one_by_id( + &db_client().await.builds, + &build.id, + mungos::update::Update::FlattenSet( + doc! { "config": config_doc }, + ), + None, + ) + .await + .context("failed to update build on database")?; + + let mut update = + make_update(&build, Operation::UpdateBuild, &user); + + update.push_simple_log( + "build update", + serde_json::to_string_pretty(&config) + .context("failed to serialize config to json")?, + ); + + update.finalize(); + + add_update(update).await?; + + let build = Build::get_resource(&build.id).await?; + Ok(build) + } +} diff --git a/bin/core/src/api/write/builder.rs b/bin/core/src/api/write/builder.rs new file mode 100644 index 000000000..30604ad6c --- /dev/null +++ b/bin/core/src/api/write/builder.rs @@ -0,0 +1,283 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + builder::{ + Builder, PartialBuilderConfig, PartialServerBuilderConfig, + }, + monitor_timestamp, + permission::PermissionLevel, + server::Server, + update::{Log, ResourceTarget, Update}, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_document}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update}, + }, + state::{db_client, State}, +}; + +#[instrument(skip(user))] +async fn validate_config( + config: &mut PartialBuilderConfig, + user: &User, +) -> anyhow::Result<()> { + match config { + PartialBuilderConfig::Server(PartialServerBuilderConfig { + server_id: Some(server_id), + }) if !server_id.is_empty() => { + let server = Server::get_resource_check_permissions( + server_id, + user, + PermissionLevel::Write, + ) + .await?; + *server_id = server.id; + } + _ => {} + } + Ok(()) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateBuilder", skip(self, user))] + async fn resolve( + &self, + CreateBuilder { name, mut config }: CreateBuilder, + user: User, + ) -> anyhow::Result { + let start_ts = monitor_timestamp(); + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + validate_config(&mut config, &user).await?; + + let builder = Builder { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: Default::default(), + }; + let builder_id = db_client() + .await + .builders + .insert_one(builder, None) + .await + .context("failed to add builder to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let builder = Builder::get_resource(&builder_id).await?; + create_permission(&user, &builder, PermissionLevel::Write).await; + let update = Update { + target: ResourceTarget::Builder(builder_id), + operation: Operation::CreateBuilder, + start_ts, + end_ts: Some(monitor_timestamp()), + operator: user.id.clone(), + success: true, + logs: vec![ + Log::simple( + "create builder", + format!( + "created builder\nid: {}\nname: {}", + builder.id, builder.name + ), + ), + Log::simple("config", format!("{:#?}", builder.config)), + ], + ..Default::default() + }; + + add_update(update).await?; + + Ok(builder) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyBuilder", skip(self, user))] + async fn resolve( + &self, + CopyBuilder { name, id }: CopyBuilder, + user: User, + ) -> anyhow::Result { + let Builder { + config, + description, + .. + } = Builder::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let start_ts = monitor_timestamp(); + let builder = Builder { + id: Default::default(), + name, + updated_at: start_ts, + description, + tags: Default::default(), + config, + info: (), + }; + let builder_id = db_client() + .await + .builders + .insert_one(builder, None) + .await + .context("failed to add builder to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let builder = Builder::get_resource(&builder_id).await?; + create_permission(&user, &builder, PermissionLevel::Write).await; + let update = Update { + target: ResourceTarget::Builder(builder_id), + operation: Operation::CreateBuilder, + start_ts, + end_ts: Some(monitor_timestamp()), + operator: user.id.clone(), + success: true, + logs: vec![ + Log::simple( + "create builder", + format!( + "created builder\nid: {}\nname: {}", + builder.id, builder.name + ), + ), + Log::simple("config", format!("{:#?}", builder.config)), + ], + ..Default::default() + }; + + add_update(update).await?; + + Ok(builder) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteBuilder", skip(self, user))] + async fn resolve( + &self, + DeleteBuilder { id }: DeleteBuilder, + user: User, + ) -> anyhow::Result { + let builder = Builder::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + // remove the builder from any attached builds + db_client() + .await + .builds + .update_many( + doc! { "config.builder.params.builder_id": &id }, + mungos::update::Update::Set( + doc! { "config.builder.params.builder_id": "" }, + ), + None, + ) + .await?; + + delete_one_by_id(&db_client().await.builders, &id, None) + .await + .context("failed to delete builder from database")?; + + delete_all_permissions_on_resource(&builder).await; + + let mut update = + make_update(&builder, Operation::DeleteBuilder, &user); + + update.push_simple_log( + "delete builder", + format!("deleted builder {}", builder.name), + ); + + update.finalize(); + add_update(update).await?; + + remove_from_recently_viewed(&builder).await?; + + Ok(builder) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateBuilder", skip(self, user))] + async fn resolve( + &self, + UpdateBuilder { id, mut config }: UpdateBuilder, + user: User, + ) -> anyhow::Result { + let builder = Builder::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + validate_config(&mut config, &user).await?; + + let mut update = Update { + target: ResourceTarget::Builder(id.clone()), + operation: Operation::UpdateBuilder, + start_ts: monitor_timestamp(), + logs: vec![Log::simple( + "builder update", + serde_json::to_string_pretty(&config) + .context("failed to serialize config update")?, + )], + operator: user.id.clone(), + ..Default::default() + }; + + let config = builder.config.merge_partial(config); + + update_one_by_id( + &db_client().await.builders, + &id, + mungos::update::Update::FlattenSet( + doc! { "config": to_document(&config)? }, + ), + None, + ) + .await?; + + let builder = Builder::get_resource(&id).await?; + + update.finalize(); + add_update(update).await?; + + Ok(builder) + } +} diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs new file mode 100644 index 000000000..66f651bb3 --- /dev/null +++ b/bin/core/src/api/write/deployment.rs @@ -0,0 +1,472 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + all_logs_success, + build::Build, + deployment::{ + Deployment, DeploymentImage, DockerContainerState, + PartialDeploymentConfig, + }, + monitor_timestamp, + permission::PermissionLevel, + server::Server, + to_monitor_name, + update::{Log, Update, UpdateStatus}, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_document}, +}; +use periphery_client::api; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, empty_or_only_spaces, periphery_client, + query::get_deployment_state, + remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[instrument(skip(user))] +async fn validate_config( + config: &mut PartialDeploymentConfig, + user: &User, +) -> anyhow::Result<()> { + if let Some(server_id) = &config.server_id { + if !server_id.is_empty() { + let server = Server::get_resource_check_permissions(server_id, user, PermissionLevel::Write) + .await + .context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?; + config.server_id = Some(server.id); + } + } + if let Some(DeploymentImage::Build { build_id, version }) = + &config.image + { + if !build_id.is_empty() { + let build = Build::get_resource_check_permissions(build_id, user, PermissionLevel::Read) + .await + .context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?; + config.image = Some(DeploymentImage::Build { + build_id: build.id, + version: version.clone(), + }); + } + } + if let Some(volumes) = &mut config.volumes { + volumes.retain(|v| { + !empty_or_only_spaces(&v.local) + && !empty_or_only_spaces(&v.container) + }) + } + if let Some(ports) = &mut config.ports { + ports.retain(|v| { + !empty_or_only_spaces(&v.local) + && !empty_or_only_spaces(&v.container) + }) + } + if let Some(environment) = &mut config.environment { + environment.retain(|v| { + !empty_or_only_spaces(&v.variable) + && !empty_or_only_spaces(&v.value) + }) + } + if let Some(extra_args) = &mut config.extra_args { + extra_args.retain(|v| !empty_or_only_spaces(v)) + } + Ok(()) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateDeployment", skip(self, user))] + async fn resolve( + &self, + CreateDeployment { name, mut config }: CreateDeployment, + user: User, + ) -> anyhow::Result { + let name = to_monitor_name(&name); + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + validate_config(&mut config, &user).await?; + let start_ts = monitor_timestamp(); + let deployment = Deployment { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: (), + }; + let deployment_id = db_client() + .await + .deployments + .insert_one(&deployment, None) + .await + .context("failed to add deployment to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let deployment = Deployment::get_resource(&deployment_id).await?; + create_permission(&user, &deployment, PermissionLevel::Write) + .await; + + let mut update = + make_update(&deployment, Operation::CreateDeployment, &user); + update.push_simple_log( + "create deployment", + format!( + "created deployment\nid: {}\nname: {}", + deployment.id, deployment.name + ), + ); + update + .push_simple_log("config", format!("{:#?}", deployment.config)); + update.finalize(); + + add_update(update).await?; + + Ok(deployment) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyDeployment", skip(self, user))] + async fn resolve( + &self, + CopyDeployment { name, id }: CopyDeployment, + user: User, + ) -> anyhow::Result { + let name = to_monitor_name(&name); + let Deployment { + config, + description, + tags, + .. + } = Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + if !config.server_id.is_empty() { + Server::get_resource_check_permissions(&config.server_id, &user, PermissionLevel::Write) + .await + .context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?; + } + if let DeploymentImage::Build { build_id, .. } = &config.image { + if !build_id.is_empty() { + Build::get_resource_check_permissions(build_id, &user, PermissionLevel::Read) + .await + .context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?; + } + } + let start_ts = monitor_timestamp(); + let deployment = Deployment { + id: Default::default(), + name, + updated_at: start_ts, + description, + tags, + config, + info: (), + }; + let deployment_id = db_client() + .await + .deployments + .insert_one(&deployment, None) + .await + .context("failed to add deployment to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let deployment = Deployment::get_resource(&deployment_id).await?; + + create_permission(&user, &deployment, PermissionLevel::Write) + .await; + + let mut update = + make_update(&deployment, Operation::CreateDeployment, &user); + update.push_simple_log( + "create deployment", + format!( + "created deployment\nid: {}\nname: {}", + deployment.id, deployment.name + ), + ); + update + .push_simple_log("config", format!("{:#?}", deployment.config)); + update.finalize(); + + add_update(update).await?; + + Ok(deployment) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteDeployment", skip(self, user))] + async fn resolve( + &self, + DeleteDeployment { id }: DeleteDeployment, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.deleting = true)?; + + let state = get_deployment_state(&deployment) + .await + .context("failed to get container state")?; + + let mut update = + make_update(&deployment, Operation::DeleteDeployment, &user); + update.in_progress(); + + update.id = add_update(update.clone()).await?; + + if !matches!( + state, + DockerContainerState::NotDeployed + | DockerContainerState::Unknown + ) { + // container needs to be destroyed + let server = + Server::get_resource(&deployment.config.server_id).await; + if let Err(e) = server { + update.logs.push(Log::error( + "remove container", + format!( + "failed to retrieve server at {} from db | {e:#?}", + deployment.config.server_id + ), + )); + } else if let Ok(server) = server { + match periphery_client(&server) { + Ok(periphery) => match periphery + .request(api::container::RemoveContainer { + name: deployment.name.clone(), + signal: deployment.config.termination_signal.into(), + time: deployment.config.termination_timeout.into(), + }) + .await + { + Ok(log) => update.logs.push(log), + Err(e) => update.push_error_log( + "remove container", + format!( + "failed to remove container on periphery | {e:#?}" + ), + ), + }, + Err(e) => update.push_error_log( + "remove container", + format!( + "failed to remove container on periphery | {e:#?}" + ), + ), + }; + } + } + + let res = delete_one_by_id( + &db_client().await.deployments, + &deployment.id, + None, + ) + .await + .context("failed to delete deployment from mongo"); + + let log = match res { + Ok(_) => Log::simple( + "delete deployment", + format!("deleted deployment {}", deployment.name), + ), + Err(e) => Log::error( + "delete deployment", + format!("failed to delete deployment\n{e:#?}"), + ), + }; + + delete_all_permissions_on_resource(&deployment).await; + + update.logs.push(log); + update.end_ts = Some(monitor_timestamp()); + update.status = UpdateStatus::Complete; + update.success = all_logs_success(&update.logs); + + update_update(update).await?; + + remove_from_recently_viewed(&deployment).await?; + + Ok(deployment) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateDeployment", skip(self, user))] + async fn resolve( + &self, + UpdateDeployment { id, mut config }: UpdateDeployment, + user: User, + ) -> anyhow::Result { + if action_states() + .deployment + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("deployment busy")); + } + + let deployment = Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = + make_update(&deployment, Operation::UpdateDeployment, &user); + + validate_config(&mut config, &user).await?; + + let config_doc = to_document(&config) + .context("failed to serialize config to bson")?; + update_one_by_id( + &db_client().await.deployments, + &id, + mungos::update::Update::FlattenSet( + doc! { "config": config_doc }, + ), + None, + ) + .await + .context("failed to update server on mongo")?; + + update.push_simple_log( + "deployment update", + serde_json::to_string_pretty(&config) + .context("failed to serialize config to json")?, + ); + update.finalize(); + + add_update(update).await?; + + let deployment: Deployment = + Deployment::get_resource(&id).await?; + + Ok(deployment) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RenameDeployment", skip(self, user))] + async fn resolve( + &self, + RenameDeployment { id, name }: RenameDeployment, + user: User, + ) -> anyhow::Result { + let deployment = Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + // get the action state for the deployment (or insert default). + let action_state = action_states() + .deployment + .get_or_insert_default(&deployment.id) + .await; + + // Will check to ensure deployment not already busy before updating, and return Err if so. + // The returned guard will set the action state back to default when dropped. + let _action_guard = + action_state.update(|state| state.renaming = true)?; + + let name = to_monitor_name(&name); + + let container_state = get_deployment_state(&deployment).await?; + + if container_state == DockerContainerState::Unknown { + return Err(anyhow!( + "cannot rename deployment when container status is unknown" + )); + } + + let mut update = + make_update(&deployment, Operation::RenameDeployment, &user); + + update_one_by_id( + &db_client().await.deployments, + &deployment.id, + mungos::update::Update::Set( + doc! { "name": &name, "updated_at": monitor_timestamp() }, + ), + None, + ) + .await + .context("failed to update deployment name on db")?; + + if container_state != DockerContainerState::NotDeployed { + let server = + Server::get_resource(&deployment.config.server_id).await?; + let log = periphery_client(&server)? + .request(api::container::RenameContainer { + curr_name: deployment.name.clone(), + new_name: name.clone(), + }) + .await + .context("failed to rename container on server")?; + update.logs.push(log); + } + + update.push_simple_log( + "rename deployment", + format!( + "renamed deployment from {} to {}", + deployment.name, name + ), + ); + update.finalize(); + + add_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/write/description.rs b/bin/core/src/api/write/description.rs new file mode 100644 index 000000000..c4f5753fb --- /dev/null +++ b/bin/core/src/api/write/description.rs @@ -0,0 +1,63 @@ +use anyhow::anyhow; +use async_trait::async_trait; +use monitor_client::{ + api::write::{UpdateDescription, UpdateDescriptionResponse}, + entities::{ + alerter::Alerter, build::Build, builder::Builder, + deployment::Deployment, procedure::Procedure, repo::Repo, + server::Server, server_template::ServerTemplate, + update::ResourceTarget, user::User, + }, +}; +use resolver_api::Resolve; + +use crate::{helpers::resource::StateResource, state::State}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateDescription", skip(self, user))] + async fn resolve( + &self, + UpdateDescription { + target, + description, + }: UpdateDescription, + user: User, + ) -> anyhow::Result { + match target { + ResourceTarget::System(_) => { + return Err(anyhow!( + "cannot update description of System resource target" + )) + } + ResourceTarget::Server(id) => { + Server::update_description(&id, &description, &user).await?; + } + ResourceTarget::Deployment(id) => { + Deployment::update_description(&id, &description, &user) + .await?; + } + ResourceTarget::Build(id) => { + Build::update_description(&id, &description, &user).await?; + } + ResourceTarget::Repo(id) => { + Repo::update_description(&id, &description, &user).await?; + } + ResourceTarget::Builder(id) => { + Builder::update_description(&id, &description, &user).await?; + } + ResourceTarget::Alerter(id) => { + Alerter::update_description(&id, &description, &user).await?; + } + ResourceTarget::Procedure(id) => { + Procedure::update_description(&id, &description, &user) + .await?; + } + ResourceTarget::ServerTemplate(id) => { + ServerTemplate::update_description(&id, &description, &user) + .await?; + } + } + Ok(UpdateDescriptionResponse {}) + } +} diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs new file mode 100644 index 000000000..23599714f --- /dev/null +++ b/bin/core/src/api/write/mod.rs @@ -0,0 +1,176 @@ +use std::time::Instant; + +use anyhow::{anyhow, Context}; +use axum::{middleware, routing::post, Extension, Router}; +use axum_extra::{headers::ContentType, TypedHeader}; +use monitor_client::{api::write::*, entities::user::User}; +use resolver_api::{derive::Resolver, Resolve, Resolver}; +use serde::{Deserialize, Serialize}; +use serror::Json; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{auth::auth_request, state::State}; + +mod alerter; +mod api_key; +mod build; +mod builder; +mod deployment; +mod description; +mod permissions; +mod procedure; +mod repo; +mod server; +mod server_template; +mod tag; +mod user; +mod user_group; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[resolver_target(State)] +#[resolver_args(User)] +#[serde(tag = "type", content = "params")] +enum WriteRequest { + // ==== API KEY ==== + CreateApiKey(CreateApiKey), + DeleteApiKey(DeleteApiKey), + CreateApiKeyForServiceUser(CreateApiKeyForServiceUser), + DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser), + + // ==== USER ==== + PushRecentlyViewed(PushRecentlyViewed), + SetLastSeenUpdate(SetLastSeenUpdate), + CreateServiceUser(CreateServiceUser), + UpdateServiceUserDescription(UpdateServiceUserDescription), + + // ==== USER GROUP ==== + CreateUserGroup(CreateUserGroup), + RenameUserGroup(RenameUserGroup), + DeleteUserGroup(DeleteUserGroup), + AddUserToUserGroup(AddUserToUserGroup), + RemoveUserFromUserGroup(RemoveUserFromUserGroup), + SetUsersInUserGroup(SetUsersInUserGroup), + + // ==== PERMISSIONS ==== + UpdateUserBasePermissions(UpdateUserBasePermissions), + UpdatePermissionOnTarget(UpdatePermissionOnTarget), + + // ==== DESCRIPTION ==== + UpdateDescription(UpdateDescription), + + // ==== SERVER ==== + CreateServer(CreateServer), + DeleteServer(DeleteServer), + UpdateServer(UpdateServer), + RenameServer(RenameServer), + CreateNetwork(CreateNetwork), + DeleteNetwork(DeleteNetwork), + + // ==== DEPLOYMENT ==== + CreateDeployment(CreateDeployment), + CopyDeployment(CopyDeployment), + DeleteDeployment(DeleteDeployment), + UpdateDeployment(UpdateDeployment), + RenameDeployment(RenameDeployment), + + // ==== BUILD ==== + CreateBuild(CreateBuild), + CopyBuild(CopyBuild), + DeleteBuild(DeleteBuild), + UpdateBuild(UpdateBuild), + + // ==== BUILDER ==== + CreateBuilder(CreateBuilder), + CopyBuilder(CopyBuilder), + DeleteBuilder(DeleteBuilder), + UpdateBuilder(UpdateBuilder), + + // ==== SERVER TEMPLATE ==== + CreateServerTemplate(CreateServerTemplate), + CopyServerTemplate(CopyServerTemplate), + DeleteServerTemplate(DeleteServerTemplate), + UpdateServerTemplate(UpdateServerTemplate), + + // ==== REPO ==== + CreateRepo(CreateRepo), + CopyRepo(CopyRepo), + DeleteRepo(DeleteRepo), + UpdateRepo(UpdateRepo), + + // ==== ALERTER ==== + CreateAlerter(CreateAlerter), + CopyAlerter(CopyAlerter), + DeleteAlerter(DeleteAlerter), + UpdateAlerter(UpdateAlerter), + + // ==== PROCEDURE ==== + CreateProcedure(CreateProcedure), + CopyProcedure(CopyProcedure), + DeleteProcedure(DeleteProcedure), + UpdateProcedure(UpdateProcedure), + + // ==== TAG ==== + CreateTag(CreateTag), + DeleteTag(DeleteTag), + RenameTag(RenameTag), + UpdateTagsOnResource(UpdateTagsOnResource), +} + +pub fn router() -> Router { + Router::new() + .route("/", post(handler)) + .layer(middleware::from_fn(auth_request)) +} + +async fn handler( + Extension(user): Extension, + Json(request): Json, +) -> serror::Result<(TypedHeader, String)> { + let req_id = Uuid::new_v4(); + + let res = tokio::spawn(task(req_id, request, user)) + .await + .context("failure in spawned task"); + + if let Err(e) = &res { + warn!("/write request {req_id} spawn error: {e:#}"); + } + + Ok((TypedHeader(ContentType::json()), res??)) +} + +#[instrument(name = "WriteRequest", skip(user))] +async fn task( + req_id: Uuid, + request: WriteRequest, + user: User, +) -> anyhow::Result { + info!( + "/write request {req_id} | user: {} ({})", + user.username, user.id + ); + + let timer = Instant::now(); + + let res = + State + .resolve_request(request, user) + .await + .map_err(|e| match e { + resolver_api::Error::Serialization(e) => { + anyhow!("{e:?}").context("response serialization error") + } + resolver_api::Error::Inner(e) => e, + }); + + if let Err(e) = &res { + warn!("/write request {req_id} error: {e:#}"); + } + + let elapsed = timer.elapsed(); + info!("/write request {req_id} | resolve time: {elapsed:?}"); + + res +} diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs new file mode 100644 index 000000000..45fa4741f --- /dev/null +++ b/bin/core/src/api/write/permissions.rs @@ -0,0 +1,311 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::{ + UpdatePermissionOnTarget, UpdatePermissionOnTargetResponse, + UpdateUserBasePermissions, UpdateUserBasePermissionsResponse, + }, + entities::{ + permission::{UserTarget, UserTargetVariant}, + update::{ResourceTarget, ResourceTargetVariant}, + user::User, + }, +}; +use mungos::{ + by_id::{find_one_by_id, update_one_by_id}, + mongodb::{ + bson::{doc, oid::ObjectId, Document}, + options::UpdateOptions, + }, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::query::get_user, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateUserBasePermissions", skip(self, admin))] + async fn resolve( + &self, + UpdateUserBasePermissions { + user_id, + enabled, + create_servers, + create_builds, + }: UpdateUserBasePermissions, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("this method is admin only")); + } + let user = find_one_by_id(&db_client().await.users, &user_id) + .await + .context("failed to query mongo for user")? + .context("did not find user with given id")?; + if user.admin { + return Err(anyhow!( + "cannot use this method to update other admins permissions" + )); + } + let mut update_doc = Document::new(); + if let Some(enabled) = enabled { + update_doc.insert("enabled", enabled); + } + if let Some(create_servers) = create_servers { + update_doc.insert("create_server_permissions", create_servers); + } + if let Some(create_builds) = create_builds { + update_doc.insert("create_build_permissions", create_builds); + } + + update_one_by_id( + &db_client().await.users, + &user_id, + mungos::update::Update::Set(update_doc), + None, + ) + .await?; + + Ok(UpdateUserBasePermissionsResponse {}) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))] + async fn resolve( + &self, + UpdatePermissionOnTarget { + user_target, + resource_target, + permission, + }: UpdatePermissionOnTarget, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("this method is admin only")); + } + + // Some extra checks if user target is an actual User + if let UserTarget::User(user_id) = &user_target { + let user = get_user(user_id).await?; + if user.admin { + return Err(anyhow!( + "cannot use this method to update other admins permissions" + )); + } + if !user.enabled { + return Err(anyhow!("user not enabled")); + } + } + + let (user_target_variant, user_target_id) = + extract_user_target_with_validation(&user_target).await?; + let (resource_variant, resource_id) = + extract_resource_target_with_validation(&resource_target) + .await?; + + let (user_target_variant, resource_variant) = + (user_target_variant.as_ref(), resource_variant.as_ref()); + + db_client() + .await + .permissions + .update_one( + doc! { + "user_target.type": user_target_variant, + "user_target.id": &user_target_id, + "resource_target.type": resource_variant, + "resource_target.id": &resource_id + }, + doc! { + "$set": { + "user_target.type": user_target_variant, + "user_target.id": user_target_id, + "resource_target.type": resource_variant, + "resource_target.id": resource_id, + "level": permission.as_ref(), + } + }, + UpdateOptions::builder().upsert(true).build(), + ) + .await?; + + Ok(UpdatePermissionOnTargetResponse {}) + } +} + +/// checks if inner id is actually a `name`, and replaces it with id if so. +async fn extract_user_target_with_validation( + user_target: &UserTarget, +) -> anyhow::Result<(UserTargetVariant, String)> { + match user_target { + UserTarget::User(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "username": ident }, + }; + let id = db_client() + .await + .users + .find_one(filter, None) + .await + .context("failed to query db for users")? + .context("no matching user found")? + .id; + Ok((UserTargetVariant::User, id)) + } + UserTarget::UserGroup(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .user_groups + .find_one(filter, None) + .await + .context("failed to query db for user_groups")? + .context("no matching user_group found")? + .id; + Ok((UserTargetVariant::UserGroup, id)) + } + } +} + +/// checks if inner id is actually a `name`, and replaces it with id if so. +async fn extract_resource_target_with_validation( + resource_target: &ResourceTarget, +) -> anyhow::Result<(ResourceTargetVariant, String)> { + match resource_target { + ResourceTarget::System(_) => { + let res = resource_target.extract_variant_id(); + Ok((res.0, res.1.clone())) + } + ResourceTarget::Build(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .builds + .find_one(filter, None) + .await + .context("failed to query db for builds")? + .context("no matching build found")? + .id; + Ok((ResourceTargetVariant::Build, id)) + } + ResourceTarget::Builder(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .builders + .find_one(filter, None) + .await + .context("failed to query db for builders")? + .context("no matching builder found")? + .id; + Ok((ResourceTargetVariant::Builder, id)) + } + ResourceTarget::Deployment(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .deployments + .find_one(filter, None) + .await + .context("failed to query db for deployments")? + .context("no matching deployment found")? + .id; + Ok((ResourceTargetVariant::Deployment, id)) + } + ResourceTarget::Server(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .servers + .find_one(filter, None) + .await + .context("failed to query db for servers")? + .context("no matching server found")? + .id; + Ok((ResourceTargetVariant::Server, id)) + } + ResourceTarget::Repo(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .repos + .find_one(filter, None) + .await + .context("failed to query db for repos")? + .context("no matching repo found")? + .id; + Ok((ResourceTargetVariant::Repo, id)) + } + ResourceTarget::Alerter(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .alerters + .find_one(filter, None) + .await + .context("failed to query db for alerters")? + .context("no matching alerter found")? + .id; + Ok((ResourceTargetVariant::Alerter, id)) + } + ResourceTarget::Procedure(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .procedures + .find_one(filter, None) + .await + .context("failed to query db for procedures")? + .context("no matching procedure found")? + .id; + Ok((ResourceTargetVariant::Procedure, id)) + } + ResourceTarget::ServerTemplate(ident) => { + let filter = match ObjectId::from_str(ident) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": ident }, + }; + let id = db_client() + .await + .server_templates + .find_one(filter, None) + .await + .context("failed to query db for server templates")? + .context("no matching server template found")? + .id; + Ok((ResourceTargetVariant::ServerTemplate, id)) + } + } +} diff --git a/bin/core/src/api/write/procedure.rs b/bin/core/src/api/write/procedure.rs new file mode 100644 index 000000000..8d03862cc --- /dev/null +++ b/bin/core/src/api/write/procedure.rs @@ -0,0 +1,405 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::{execute::Execution, write::*}, + entities::{ + build::Build, + deployment::Deployment, + monitor_timestamp, + permission::PermissionLevel, + procedure::{PartialProcedureConfig, Procedure}, + repo::Repo, + server::Server, + update::Log, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_document}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateProcedure", skip(self, user))] + async fn resolve( + &self, + CreateProcedure { name, mut config }: CreateProcedure, + user: User, + ) -> anyhow::Result { + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + + let start_ts = monitor_timestamp(); + + validate_procedure_config(&mut config, &user, None).await?; + + let procedure = Procedure { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + info: Default::default(), + config: config.into(), + }; + let procedure_id = db_client() + .await + .procedures + .insert_one(procedure, None) + .await + .context("failed to add procedure to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let procedure = Procedure::get_resource(&procedure_id).await?; + + create_permission(&user, &procedure, PermissionLevel::Write) + .await; + + let mut update = + make_update(&procedure, Operation::CreateProcedure, &user); + + update.push_simple_log( + "create procedure", + format!( + "created procedure\nid: {}\nname: {}", + procedure.id, procedure.name + ), + ); + + update + .push_simple_log("config", format!("{:#?}", procedure.config)); + + update.finalize(); + + add_update(update).await?; + + Ok(procedure) + } +} + +#[instrument] +async fn validate_procedure_config( + config: &mut PartialProcedureConfig, + user: &User, + id: Option<&str>, +) -> anyhow::Result<()> { + let Some(executions) = &mut config.executions else { + return Ok(()); + }; + for exec in executions { + match &mut exec.execution { + Execution::None(_) => {} + Execution::RunProcedure(params) => { + let procedure = Procedure::get_resource_check_permissions( + ¶ms.procedure, + user, + PermissionLevel::Execute, + ) + .await?; + match id { + Some(id) if procedure.id == id => { + return Err(anyhow!( + "Cannot have self-referential procedure" + )) + } + _ => {} + } + params.procedure = procedure.id; + } + Execution::RunBuild(params) => { + let build = Build::get_resource_check_permissions( + ¶ms.build, + user, + PermissionLevel::Execute, + ) + .await?; + params.build = build.id; + } + Execution::Deploy(params) => { + let deployment = Deployment::get_resource_check_permissions( + ¶ms.deployment, + user, + PermissionLevel::Execute, + ) + .await?; + params.deployment = deployment.id; + } + Execution::StartContainer(params) => { + let deployment = Deployment::get_resource_check_permissions( + ¶ms.deployment, + user, + PermissionLevel::Execute, + ) + .await?; + params.deployment = deployment.id; + } + Execution::StopContainer(params) => { + let deployment = Deployment::get_resource_check_permissions( + ¶ms.deployment, + user, + PermissionLevel::Execute, + ) + .await?; + params.deployment = deployment.id; + } + Execution::StopAllContainers(params) => { + let server = Server::get_resource_check_permissions( + ¶ms.server, + user, + PermissionLevel::Execute, + ) + .await?; + params.server = server.id; + } + Execution::RemoveContainer(params) => { + let deployment = Deployment::get_resource_check_permissions( + ¶ms.deployment, + user, + PermissionLevel::Execute, + ) + .await?; + params.deployment = deployment.id; + } + Execution::CloneRepo(params) => { + let repo = Repo::get_resource_check_permissions( + ¶ms.repo, + user, + PermissionLevel::Execute, + ) + .await?; + params.repo = repo.id; + } + Execution::PullRepo(params) => { + let repo = Repo::get_resource_check_permissions( + ¶ms.repo, + user, + PermissionLevel::Execute, + ) + .await?; + params.repo = repo.id; + } + Execution::PruneDockerNetworks(params) => { + let server = Server::get_resource_check_permissions( + ¶ms.server, + user, + PermissionLevel::Execute, + ) + .await?; + params.server = server.id; + } + Execution::PruneDockerImages(params) => { + let server = Server::get_resource_check_permissions( + ¶ms.server, + user, + PermissionLevel::Execute, + ) + .await?; + params.server = server.id; + } + Execution::PruneDockerContainers(params) => { + let server = Server::get_resource_check_permissions( + ¶ms.server, + user, + PermissionLevel::Execute, + ) + .await?; + params.server = server.id; + } + } + } + Ok(()) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyProcedure", skip(self, user))] + async fn resolve( + &self, + CopyProcedure { name, id }: CopyProcedure, + user: User, + ) -> anyhow::Result { + let Procedure { + config, + description, + tags, + .. + } = Procedure::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let start_ts = monitor_timestamp(); + let build = Procedure { + id: Default::default(), + name, + updated_at: start_ts, + description, + tags, + config, + info: Default::default(), + }; + let procedure_id = db_client() + .await + .procedures + .insert_one(build, None) + .await + .context("failed to add build to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let procedure = Procedure::get_resource(&procedure_id).await?; + + create_permission(&user, &procedure, PermissionLevel::Write) + .await; + + let mut update = + make_update(&procedure, Operation::CreateProcedure, &user); + + update.push_simple_log( + "create procedure", + format!( + "created procedure\nid: {}\nname: {}", + procedure.id, procedure.name + ), + ); + update.push_simple_log( + "config", + serde_json::to_string_pretty(&procedure)?, + ); + + update.finalize(); + + add_update(update).await?; + + Ok(procedure) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateProcedure", skip(self, user))] + async fn resolve( + &self, + UpdateProcedure { id, mut config }: UpdateProcedure, + user: User, + ) -> anyhow::Result { + let procedure = Procedure::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + validate_procedure_config( + &mut config, + &user, + Some(&procedure.id), + ) + .await?; + + update_one_by_id( + &db_client().await.procedures, + &procedure.id, + mungos::update::Update::FlattenSet( + doc! { "config": to_document(&config)? }, + ), + None, + ) + .await + .context("failed to update procedure on database")?; + + let mut update = + make_update(&procedure, Operation::UpdateProcedure, &user); + + update.push_simple_log( + "procedure update", + serde_json::to_string_pretty(&config)?, + ); + + update.finalize(); + + add_update(update).await?; + + let procedure = Procedure::get_resource(&procedure.id).await?; + + Ok(procedure) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteProcedure", skip(self, user))] + async fn resolve( + &self, + DeleteProcedure { id }: DeleteProcedure, + user: User, + ) -> anyhow::Result { + // needs to pull its id from all container procedures + if action_states() + .procedure + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("procedure busy")); + } + + let procedure = Procedure::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = + make_update(&procedure, Operation::DeleteProcedure, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let res = + delete_one_by_id(&db_client().await.procedures, &id, None) + .await + .context("failed to delete build from database"); + + delete_all_permissions_on_resource(&procedure).await; + + let log = match res { + Ok(_) => Log::simple( + "delete procedure", + format!("deleted procedure {}", procedure.name), + ), + Err(e) => Log::error( + "delete procedure", + format!("failed to delete procedure\n{e:#?}"), + ), + }; + + update.logs.push(log); + update.finalize(); + update_update(update).await?; + + remove_from_recently_viewed(&procedure).await?; + + Ok(procedure) + } +} diff --git a/bin/core/src/api/write/repo.rs b/bin/core/src/api/write/repo.rs new file mode 100644 index 000000000..4e5d172e7 --- /dev/null +++ b/bin/core/src/api/write/repo.rs @@ -0,0 +1,358 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::{execute, write::*}, + entities::{ + monitor_timestamp, + permission::PermissionLevel, + repo::{PartialRepoConfig, Repo}, + server::Server, + to_monitor_name, + update::{Log, ResourceTarget, Update}, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; +use periphery_client::api; +use resolver_api::Resolve; +use serror::serialize_error_pretty; + +use crate::{ + helpers::{ + create_permission, periphery_client, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update, update_update}, + }, + state::{action_states, db_client, State}, +}; + +#[instrument(skip(user))] +async fn validate_config( + config: &mut PartialRepoConfig, + user: &User, +) -> anyhow::Result<()> { + match &config.server_id { + Some(server_id) if !server_id.is_empty() => { + let server = Server::get_resource_check_permissions( + server_id, + user, + PermissionLevel::Write, + ) + .await + .context("cannot create repo on this server. user must have update permissions on the server.")?; + config.server_id = Some(server.id); + } + _ => {} + } + Ok(()) +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateRepo", skip(self, user))] + async fn resolve( + &self, + CreateRepo { name, mut config }: CreateRepo, + user: User, + ) -> anyhow::Result { + let name = to_monitor_name(&name); + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + validate_config(&mut config, &user).await?; + let start_ts = monitor_timestamp(); + let repo = Repo { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: Default::default(), + }; + let repo_id = db_client() + .await + .repos + .insert_one(repo, None) + .await + .context("failed to add repo to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + + let repo = Repo::get_resource(&repo_id).await?; + + create_permission(&user, &repo, PermissionLevel::Write).await; + + let update = Update { + target: ResourceTarget::Repo(repo_id), + operation: Operation::CreateRepo, + start_ts, + end_ts: Some(monitor_timestamp()), + operator: user.id.clone(), + success: true, + logs: vec![ + Log::simple( + "create repo", + format!( + "created repo\nid: {}\nname: {}", + repo.id, repo.name + ), + ), + Log::simple("config", format!("{:#?}", repo.config)), + ], + ..Default::default() + }; + + add_update(update).await?; + + if !repo.config.repo.is_empty() + && !repo.config.server_id.is_empty() + { + let _ = self + .resolve( + execute::CloneRepo { + repo: repo.id.clone(), + }, + user, + ) + .await; + } + + Ok(repo) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CopyRepo", skip(self, user))] + async fn resolve( + &self, + CopyRepo { name, id }: CopyRepo, + user: User, + ) -> anyhow::Result { + let Repo { + config, + description, + tags, + .. + } = Repo::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + if !config.server_id.is_empty() { + Server::get_resource_check_permissions( + &config.server_id, + &user, + PermissionLevel::Write, + ) + .await + .context("cannot create repo on this server. user must have update permissions on the server.")?; + } + let start_ts = monitor_timestamp(); + let repo = Repo { + id: Default::default(), + name, + updated_at: start_ts, + description, + tags, + config, + info: Default::default(), + }; + let repo_id = db_client() + .await + .repos + .insert_one(repo, None) + .await + .context("failed to add repo to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let repo = Repo::get_resource(&repo_id).await?; + create_permission(&user, &repo, PermissionLevel::Write).await; + let mut update = make_update(&repo, Operation::CreateRepo, &user); + update.push_simple_log( + "create repo", + format!("created repo\nid: {}\nname: {}", repo.id, repo.name), + ); + update.push_simple_log("config", format!("{:#?}", repo.config)); + update.finalize(); + + add_update(update).await?; + + Ok(repo) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteRepo", skip(self, user))] + async fn resolve( + &self, + DeleteRepo { id }: DeleteRepo, + user: User, + ) -> anyhow::Result { + let repo = Repo::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + // get the action state for the repo (or insert default). + let action_state = + action_states().repo.get_or_insert_default(&repo.id).await; + + // This will set action state back to default when dropped. + // Will also check to ensure repo not already busy before updating. + let _action_guard = + action_state.update(|state| state.deleting = true)?; + + let periphery = if repo.config.server_id.is_empty() { + None + } else { + let server = + Server::get_resource(&repo.config.server_id).await?; + let periphery = periphery_client(&server)?; + Some(periphery) + }; + + let mut update = make_update(&repo, Operation::DeleteRepo, &user); + update.in_progress(); + update.id = add_update(update.clone()).await?; + + let res = + delete_one_by_id(&db_client().await.repos, &repo.id, None) + .await + .context("failed to delete repo from database"); + + delete_all_permissions_on_resource(&repo).await; + + let log = match res { + Ok(_) => Log::simple( + "delete repo", + format!("deleted repo {}", repo.name), + ), + Err(e) => Log::error( + "delete repo", + format!("failed to delete repo\n{e:#?}"), + ), + }; + + update.logs.push(log); + + if let Some(periphery) = periphery { + match periphery + .request(api::git::DeleteRepo { + name: repo.name.clone(), + }) + .await + { + Ok(log) => update.logs.push(log), + Err(e) => update.logs.push(Log::error( + "delete repo on periphery", + serialize_error_pretty(&e), + )), + } + } + + update.finalize(); + update_update(update).await?; + + remove_from_recently_viewed(&repo).await?; + + Ok(repo) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateRepo", skip(self, user))] + async fn resolve( + &self, + UpdateRepo { id, mut config }: UpdateRepo, + user: User, + ) -> anyhow::Result { + validate_config(&mut config, &user).await?; + + let repo = Repo::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + // get the action state for the repo (or insert default). + let action_state = + action_states().repo.get_or_insert_default(&repo.id).await; + + // This will set action state back to default when dropped. + // Will also check to ensure repo not already busy before updating. + let _action_guard = + action_state.update(|state| state.updating = true)?; + + update_one_by_id( + &db_client().await.repos, + &repo.id, + mungos::update::Update::FlattenSet( + doc! { "config": to_bson(&config)? }, + ), + None, + ) + .await + .context("failed to update repo on database")?; + + let mut update = make_update(&repo, Operation::UpdateRepo, &user); + update.in_progress(); + update.push_simple_log( + "repo update", + serde_json::to_string_pretty(&config).unwrap(), + ); + update.id = add_update(update.clone()).await?; + + if let Some(new_server_id) = config.server_id { + if !repo.config.server_id.is_empty() + && new_server_id != repo.config.server_id + { + let old_server: anyhow::Result = + Server::get_resource(&repo.config.server_id).await; + let periphery = + old_server.and_then(|server| periphery_client(&server)); + match periphery { + Ok(periphery) => match periphery + .request(api::git::DeleteRepo { name: repo.name }) + .await + { + Ok(mut log) => { + log.stage = String::from("cleanup previous server"); + update.logs.push(log); + } + Err(e) => update.push_error_log( + "cleanup previous server", + format!("failed to cleanup previous server | {e:#?}"), + ), + }, + Err(e) => update.push_error_log( + "cleanup previous server", + format!("failed to cleanup previous server | {e:#?}"), + ), + } + } + } + + update.finalize(); + update_update(update).await?; + + Repo::get_resource(&repo.id).await + } +} diff --git a/bin/core/src/api/write/server.rs b/bin/core/src/api/write/server.rs new file mode 100644 index 000000000..6d78530bd --- /dev/null +++ b/bin/core/src/api/write/server.rs @@ -0,0 +1,356 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + monitor_timestamp, + permission::PermissionLevel, + server::Server, + update::{Log, ResourceTarget, Update, UpdateStatus}, + user::User, + Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; +use periphery_client::api; +use resolver_api::Resolve; +use serror::serialize_error_pretty; + +use crate::{ + helpers::{ + create_permission, periphery_client, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update, update_update}, + }, + monitor::update_cache_for_server, + state::{action_states, db_client, server_status_cache, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateServer", skip(self, user))] + async fn resolve( + &self, + CreateServer { name, config }: CreateServer, + user: User, + ) -> anyhow::Result { + if !user.admin && !user.create_server_permissions { + return Err(anyhow!( + "user does not have create server permissions" + )); + } + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + let start_ts = monitor_timestamp(); + let server = Server { + id: Default::default(), + name, + updated_at: start_ts, + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: (), + }; + let server_id = db_client() + .await + .servers + .insert_one(&server, None) + .await + .context("failed to add server to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let server = Server::get_resource(&server_id).await?; + create_permission(&user, &server, PermissionLevel::Write).await; + let update = Update { + target: ResourceTarget::Server(server_id), + operation: Operation::CreateServer, + start_ts, + end_ts: Some(monitor_timestamp()), + operator: user.id.clone(), + success: true, + logs: vec![ + Log::simple( + "create server", + format!( + "created server\nid: {}\nname: {}", + server.id, server.name + ), + ), + Log::simple("config", format!("{:#?}", server.config)), + ], + ..Default::default() + }; + + add_update(update).await?; + + update_cache_for_server(&server).await; + + Ok(server) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteServer", skip(self, user))] + async fn resolve( + &self, + DeleteServer { id }: DeleteServer, + user: User, + ) -> anyhow::Result { + if action_states() + .server + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("server busy")); + } + + let server = Server::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + db_client() + .await + .builders + .update_many( + doc! { "config.params.server_id": &id }, + doc! { "$set": { "config.params.server_id": "" } }, + None, + ) + .await + .context("failed to detach server from builders")?; + + db_client() + .await + .deployments + .update_many( + doc! { "config.server_id": &id }, + doc! { "$set": { "config.server_id": "" } }, + None, + ) + .await + .context("failed to detach server from deployments")?; + + db_client() + .await + .repos + .update_many( + doc! { "config.server_id": &id }, + doc! { "$set": { "config.server_id": "" } }, + None, + ) + .await + .context("failed to detach server from repos")?; + + db_client() + .await + .alerts + .update_many( + doc! { "target.type": "Server", "target.id": &id }, + doc! { "$set": { + "resolved": true, + "resolved_ts": unix_timestamp_ms() as i64 + } }, + None, + ) + .await + .context("failed to detach server from repos")?; + + delete_one_by_id(&db_client().await.servers, &id, None) + .await + .context("failed to delete server from mongo")?; + + delete_all_permissions_on_resource(&server).await; + + let mut update = + make_update(&server, Operation::DeleteServer, &user); + update.push_simple_log( + "delete server", + format!("deleted server {}", server.name), + ); + + update.finalize(); + add_update(update).await?; + + server_status_cache().remove(&id).await; + + remove_from_recently_viewed(&server).await?; + + Ok(server) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateServer", skip(self, user))] + async fn resolve( + &self, + UpdateServer { id, config }: UpdateServer, + user: User, + ) -> anyhow::Result { + if action_states() + .server + .get(&id) + .await + .unwrap_or_default() + .busy()? + { + return Err(anyhow!("server busy")); + } + + let server = Server::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let mut update = + make_update(&server, Operation::UpdateServer, &user); + + update_one_by_id( + &db_client().await.servers, + &id, + mungos::update::Update::FlattenSet( + doc! { "config": to_bson(&config)? }, + ), + None, + ) + .await + .context("failed to update server on mongo")?; + + update.push_simple_log( + "server update", + serde_json::to_string_pretty(&config)?, + ); + + let new_server = Server::get_resource(&id).await?; + + update_cache_for_server(&new_server).await; + + update.finalize(); + + add_update(update).await?; + + Ok(new_server) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RenameServer", skip(self, user))] + async fn resolve( + &self, + RenameServer { id, name }: RenameServer, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let mut update = + make_update(&server, Operation::RenameServer, &user); + + update_one_by_id(&db_client().await.servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": monitor_timestamp() }), None) + .await + .context("failed to update server on db. this name may already be taken.")?; + update.push_simple_log( + "rename server", + format!("renamed server {id} from {} to {name}", server.name), + ); + update.finalize(); + update.id = add_update(update.clone()).await?; + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateNetwork", skip(self, user))] + async fn resolve( + &self, + CreateNetwork { server, name }: CreateNetwork, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Write, + ) + .await?; + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&server, Operation::CreateNetwork, &user); + update.status = UpdateStatus::InProgress; + update.id = add_update(update.clone()).await?; + + match periphery + .request(api::network::CreateNetwork { name, driver: None }) + .await + { + Ok(log) => update.logs.push(log), + Err(e) => update + .push_error_log("create network", serialize_error_pretty(&e)), + }; + + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteNetwork", skip(self, user))] + async fn resolve( + &self, + DeleteNetwork { server, name }: DeleteNetwork, + user: User, + ) -> anyhow::Result { + let server = Server::get_resource_check_permissions( + &server, + &user, + PermissionLevel::Write, + ) + .await?; + + let periphery = periphery_client(&server)?; + + let mut update = + make_update(&server, Operation::DeleteNetwork, &user); + update.status = UpdateStatus::InProgress; + update.id = add_update(update.clone()).await?; + + match periphery + .request(api::network::DeleteNetwork { name }) + .await + { + Ok(log) => update.logs.push(log), + Err(e) => update + .push_error_log("delete network", serialize_error_pretty(&e)), + }; + + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/write/server_template.rs b/bin/core/src/api/write/server_template.rs new file mode 100644 index 000000000..1115a6815 --- /dev/null +++ b/bin/core/src/api/write/server_template.rs @@ -0,0 +1,249 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use axum::async_trait; +use monitor_client::{ + api::write::{ + CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate, + UpdateServerTemplate, + }, + entities::{ + monitor_timestamp, permission::PermissionLevel, + server_template::ServerTemplate, user::User, Operation, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId, to_document}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + create_permission, remove_from_recently_viewed, + resource::{delete_all_permissions_on_resource, StateResource}, + update::{add_update, make_update}, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CreateServerTemplate { name, config }: CreateServerTemplate, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("only admins can create server templates")); + } + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("valid ObjectIds cannot be used as names")); + } + let server_template = ServerTemplate { + id: Default::default(), + name, + updated_at: monitor_timestamp(), + description: Default::default(), + tags: Default::default(), + config: config.into(), + info: (), + }; + let server_template_id = db_client() + .await + .server_templates + .insert_one(server_template, None) + .await + .context("failed to add server_template to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let server_template = + ServerTemplate::get_resource(&server_template_id).await?; + create_permission( + &user, + &server_template, + PermissionLevel::Write, + ) + .await; + let mut update = make_update( + &server_template, + Operation::CreateServerTemplate, + &user, + ); + update.push_simple_log( + "create server template", + format!( + "created server template\nid: {}\nname: {}", + server_template.id, server_template.name + ), + ); + update.push_simple_log( + "config", + format!("{:#?}", server_template.config), + ); + update.finalize(); + + add_update(update).await?; + + Ok(server_template) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CopyServerTemplate { name, id }: CopyServerTemplate, + user: User, + ) -> anyhow::Result { + let ServerTemplate { + config, + description, + .. + } = ServerTemplate::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + let server_template = ServerTemplate { + id: Default::default(), + name, + updated_at: monitor_timestamp(), + description, + tags: Default::default(), + config, + info: (), + }; + let server_template_id = db_client() + .await + .server_templates + .insert_one(server_template, None) + .await + .context("failed to add server_template to db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + let server_template = + ServerTemplate::get_resource(&server_template_id).await?; + create_permission( + &user, + &server_template, + PermissionLevel::Write, + ) + .await; + let mut update = make_update( + &server_template, + Operation::CreateServerTemplate, + &user, + ); + update.push_simple_log( + "create server template", + format!( + "created server template\nid: {}\nname: {}", + server_template.id, server_template.name + ), + ); + update.push_simple_log( + "config", + format!("{:#?}", server_template.config), + ); + update.finalize(); + + add_update(update).await?; + + Ok(server_template) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + DeleteServerTemplate { id }: DeleteServerTemplate, + user: User, + ) -> anyhow::Result { + let server_template = + ServerTemplate::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + delete_one_by_id(&db_client().await.server_templates, &id, None) + .await + .context("failed to delete server templates from database")?; + + delete_all_permissions_on_resource(&server_template).await; + + let mut update = make_update( + &server_template, + Operation::DeleteServerTemplate, + &user, + ); + + update.push_simple_log( + "delete server template", + format!("deleted server template {}", server_template.name), + ); + + update.finalize(); + add_update(update).await?; + + remove_from_recently_viewed(&server_template).await?; + + Ok(server_template) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + UpdateServerTemplate { id, config }: UpdateServerTemplate, + user: User, + ) -> anyhow::Result { + let server_template = + ServerTemplate::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + + let mut update = make_update( + &server_template, + Operation::UpdateServerTemplate, + &user, + ); + + update.push_simple_log( + "server template update", + serde_json::to_string_pretty(&config) + .context("failed to serialize config update")?, + ); + + let config = server_template.config.merge_partial(config); + let config = to_document(&config) + .context("failed to serialize update to bson document")?; + + update_one_by_id( + &db_client().await.server_templates, + &id, + mungos::update::Update::FlattenSet(doc! { "config": config }), + None, + ) + .await?; + + let server_template = ServerTemplate::get_resource(&id).await?; + + update.finalize(); + add_update(update).await?; + + Ok(server_template) + } +} diff --git a/bin/core/src/api/write/tag.rs b/bin/core/src/api/write/tag.rs new file mode 100644 index 000000000..912b63fd8 --- /dev/null +++ b/bin/core/src/api/write/tag.rs @@ -0,0 +1,203 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::{ + CreateTag, DeleteTag, RenameTag, UpdateTagsOnResource, + UpdateTagsOnResourceResponse, + }, + entities::{ + alerter::Alerter, build::Build, builder::Builder, + deployment::Deployment, permission::PermissionLevel, + procedure::Procedure, repo::Repo, server::Server, + server_template::ServerTemplate, tag::Tag, + update::ResourceTarget, user::User, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::{ + query::{get_tag, get_tag_check_owner}, + resource::StateResource, + }, + state::{db_client, State}, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateTag", skip(self, user))] + async fn resolve( + &self, + CreateTag { name }: CreateTag, + user: User, + ) -> anyhow::Result { + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("tag name cannot be ObjectId")); + } + + let mut tag = Tag { + id: Default::default(), + name, + owner: user.id.clone(), + }; + + tag.id = db_client() + .await + .tags + .insert_one(&tag, None) + .await + .context("failed to create tag on db")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + + Ok(tag) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + RenameTag { id, name }: RenameTag, + user: User, + ) -> anyhow::Result { + if ObjectId::from_str(&name).is_ok() { + return Err(anyhow!("tag name cannot be ObjectId")); + } + + get_tag_check_owner(&id, &user).await?; + + update_one_by_id( + &db_client().await.tags, + &id, + doc! { "$set": { "name": name } }, + None, + ) + .await + .context("failed to rename tag on db")?; + + get_tag(&id).await + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteTag", skip(self, user))] + async fn resolve( + &self, + DeleteTag { id }: DeleteTag, + user: User, + ) -> anyhow::Result { + let tag = get_tag_check_owner(&id, &user).await?; + + tokio::try_join!( + Server::remove_tag_from_resources(&id,), + Deployment::remove_tag_from_resources(&id,), + Build::remove_tag_from_resources(&id,), + Repo::remove_tag_from_resources(&id,), + Builder::remove_tag_from_resources(&id,), + Alerter::remove_tag_from_resources(&id,), + Procedure::remove_tag_from_resources(&id,), + )?; + + delete_one_by_id(&db_client().await.tags, &id, None).await?; + + Ok(tag) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "UpdateTagsOnResource", skip(self, user))] + async fn resolve( + &self, + UpdateTagsOnResource { target, tags }: UpdateTagsOnResource, + user: User, + ) -> anyhow::Result { + match target { + ResourceTarget::System(_) => return Err(anyhow!("")), + ResourceTarget::Build(id) => { + Build::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Build::update_tags_on_resource(&id, tags, user).await?; + } + ResourceTarget::Builder(id) => { + Builder::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Builder::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::Deployment(id) => { + Deployment::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Deployment::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::Server(id) => { + Server::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Server::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::Repo(id) => { + Repo::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Repo::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::Alerter(id) => { + Alerter::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Alerter::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::Procedure(id) => { + Procedure::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + Procedure::update_tags_on_resource(&id, tags, user).await? + } + ResourceTarget::ServerTemplate(id) => { + ServerTemplate::get_resource_check_permissions( + &id, + &user, + PermissionLevel::Write, + ) + .await?; + ServerTemplate::update_tags_on_resource(&id, tags, user) + .await? + } + }; + Ok(UpdateTagsOnResourceResponse {}) + } +} diff --git a/bin/core/src/api/write/user.rs b/bin/core/src/api/write/user.rs new file mode 100644 index 000000000..c2677c0c2 --- /dev/null +++ b/bin/core/src/api/write/user.rs @@ -0,0 +1,173 @@ +use std::{collections::VecDeque, str::FromStr}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::{ + CreateServiceUser, CreateServiceUserResponse, PushRecentlyViewed, + PushRecentlyViewedResponse, SetLastSeenUpdate, + SetLastSeenUpdateResponse, UpdateServiceUserDescription, + UpdateServiceUserDescriptionResponse, + }, + entities::{ + monitor_timestamp, + user::{User, UserConfig}, + }, +}; +use mungos::{ + by_id::update_one_by_id, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::query::get_user, + state::{db_client, State}, +}; + +const RECENTLY_VIEWED_MAX: usize = 10; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PushRecentlyViewed", skip(self, user))] + async fn resolve( + &self, + PushRecentlyViewed { resource }: PushRecentlyViewed, + user: User, + ) -> anyhow::Result { + let mut recently_viewed = get_user(&user.id) + .await? + .recently_viewed + .into_iter() + .filter(|r| !resource.eq(r)) + .take(RECENTLY_VIEWED_MAX - 1) + .collect::>(); + + recently_viewed.push_front(resource); + + let recently_viewed = to_bson(&recently_viewed) + .context("failed to convert recently views to bson")?; + + update_one_by_id( + &db_client().await.users, + &user.id, + mungos::update::Update::Set(doc! { + "recently_viewed": recently_viewed + }), + None, + ) + .await + .context("context")?; + + Ok(PushRecentlyViewedResponse {}) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "SetLastSeenUpdate", skip(self, user))] + async fn resolve( + &self, + SetLastSeenUpdate {}: SetLastSeenUpdate, + user: User, + ) -> anyhow::Result { + update_one_by_id( + &db_client().await.users, + &user.id, + mungos::update::Update::Set(doc! { + "last_update_view": monitor_timestamp() + }), + None, + ) + .await + .context("failed to update user last_update_view")?; + Ok(SetLastSeenUpdateResponse {}) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateServiceUser", skip(self, user))] + async fn resolve( + &self, + CreateServiceUser { + username, + description, + }: CreateServiceUser, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("user not admin")); + } + if ObjectId::from_str(&username).is_ok() { + return Err(anyhow!("username cannot be valid ObjectId")); + } + let config = UserConfig::Service { description }; + let mut user = User { + id: Default::default(), + username, + config, + enabled: true, + admin: false, + create_server_permissions: false, + create_build_permissions: false, + last_update_view: 0, + recently_viewed: Vec::new(), + updated_at: monitor_timestamp(), + }; + user.id = db_client() + .await + .users + .insert_one(&user, None) + .await + .context("failed to create service user on db")? + .inserted_id + .as_object_id() + .context("inserted id is not object id")? + .to_string(); + Ok(user) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument( + name = "UpdateServiceUserDescription", + skip(self, user) + )] + async fn resolve( + &self, + UpdateServiceUserDescription { + username, + description, + }: UpdateServiceUserDescription, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("user not admin")); + } + let db = db_client().await; + let service_user = db + .users + .find_one(doc! { "username": &username }, None) + .await + .context("failed to query db for user")? + .context("no user with given username")?; + let UserConfig::Service { .. } = &service_user.config else { + return Err(anyhow!("user is not service user")); + }; + db.users + .update_one( + doc! { "username": &username }, + doc! { "$set": { "config.data.description": description } }, + None, + ) + .await + .context("failed to update user on db")?; + db.users + .find_one(doc! { "username": &username }, None) + .await + .context("failed to query db for user")? + .context("user with username not found") + } +} diff --git a/bin/core/src/api/write/user_group.rs b/bin/core/src/api/write/user_group.rs new file mode 100644 index 000000000..3bfb6bb81 --- /dev/null +++ b/bin/core/src/api/write/user_group.rs @@ -0,0 +1,252 @@ +use std::{collections::HashMap, str::FromStr}; + +use anyhow::{anyhow, Context}; +use axum::async_trait; +use monitor_client::{ + api::write::{ + AddUserToUserGroup, CreateUserGroup, DeleteUserGroup, + RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup, + }, + entities::{monitor_timestamp, user::User, user_group::UserGroup}, +}; +use mungos::{ + by_id::{delete_one_by_id, find_one_by_id, update_one_by_id}, + find::find_collect, + mongodb::bson::{doc, oid::ObjectId}, +}; +use resolver_api::Resolve; + +use crate::state::{db_client, State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CreateUserGroup { name }: CreateUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let user_group = UserGroup { + id: Default::default(), + users: Default::default(), + updated_at: monitor_timestamp(), + name, + }; + let db = db_client().await; + let id = db + .user_groups + .insert_one(user_group, None) + .await + .context("failed to create UserGroup on db")? + .inserted_id + .as_object_id() + .context("inserted id is not ObjectId")? + .to_string(); + find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for user groups")? + .context("user group at id not found") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + RenameUserGroup { id, name }: RenameUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let db = db_client().await; + update_one_by_id( + &db.user_groups, + &id, + doc! { "$set": { "name": name } }, + None, + ) + .await + .context("failed to rename UserGroup on db")?; + find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + DeleteUserGroup { id }: DeleteUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + + let db = db_client().await; + + let ug = find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for UserGroups")? + .context("no UserGroup found with given id")?; + + delete_one_by_id(&db.user_groups, &id, None) + .await + .context("failed to delete UserGroup from db")?; + + db.permissions + .delete_many(doc! { + "user_target.type": "UserGroup", + "user_target.id": id, + }, None) + .await + .context("failed to clean up UserGroups permissions. User Group has been deleted")?; + + Ok(ug) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + AddUserToUserGroup { user_group, user }: AddUserToUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + + let db = db_client().await; + + let filter = match ObjectId::from_str(&user) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "username": &user }, + }; + let user = db + .users + .find_one(filter, None) + .await + .context("failed to query mongo for users")? + .context("no matching user found")?; + + let filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + db.user_groups + .update_one( + filter.clone(), + doc! { "$addToSet": { "users": &user.id } }, + None, + ) + .await + .context("failed to add user to group on db")?; + db.user_groups + .find_one(filter, None) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + RemoveUserFromUserGroup { + user_group, + user, + }: RemoveUserFromUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + + let db = db_client().await; + + let filter = match ObjectId::from_str(&user) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "username": &user }, + }; + let user = db + .users + .find_one(filter, None) + .await + .context("failed to query mongo for users")? + .context("no matching user found")?; + + let filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + db.user_groups + .update_one( + filter.clone(), + doc! { "$pull": { "users": &user.id } }, + None, + ) + .await + .context("failed to add user to group on db")?; + db.user_groups + .find_one(filter, None) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + SetUsersInUserGroup { user_group, users }: SetUsersInUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + + let db = db_client().await; + + let all_users = find_collect(&db.users, None, None) + .await + .context("failed to query db for users")? + .into_iter() + .map(|u| (u.username, u.id)) + .collect::>(); + + // Make sure all users are user ids + let users = users + .into_iter() + .filter_map(|user| match ObjectId::from_str(&user) { + Ok(_) => Some(user), + Err(_) => all_users.get(&user).cloned(), + }) + .collect::>(); + + let filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + db.user_groups + .update_one( + filter.clone(), + doc! { "$set": { "users": users } }, + None, + ) + .await + .context("failed to add user to group on db")?; + db.user_groups + .find_one(filter, None) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} diff --git a/bin/core/src/auth/github/client.rs b/bin/core/src/auth/github/client.rs new file mode 100644 index 000000000..9d4ae178e --- /dev/null +++ b/bin/core/src/auth/github/client.rs @@ -0,0 +1,229 @@ +use std::sync::OnceLock; + +use anyhow::{anyhow, Context}; +use monitor_client::entities::config::core::{ + CoreConfig, OauthCredentials, +}; +use reqwest::StatusCode; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tokio::sync::Mutex; + +use crate::{ + auth::{random_string, STATE_PREFIX_LENGTH}, + config::core_config, +}; + +pub fn github_oauth_client() -> &'static Option { + static GITHUB_OAUTH_CLIENT: OnceLock> = + OnceLock::new(); + GITHUB_OAUTH_CLIENT + .get_or_init(|| GithubOauthClient::new(core_config())) +} + +pub struct GithubOauthClient { + http: reqwest::Client, + client_id: String, + client_secret: String, + redirect_uri: String, + scopes: String, + states: Mutex>, + user_agent: String, +} + +impl GithubOauthClient { + pub fn new( + CoreConfig { + github_oauth: + OauthCredentials { + enabled, + id, + secret, + }, + host, + .. + }: &CoreConfig, + ) -> Option { + if !enabled { + return None; + } + if host.is_empty() { + warn!("github oauth is enabled, but 'config.host' is not configured"); + return None; + } + if id.is_empty() { + warn!("github oauth is enabled, but 'config.github_oauth.id' is not configured"); + return None; + } + if secret.is_empty() { + warn!("github oauth is enabled, but 'config.github_oauth.secret' is not configured"); + return None; + } + GithubOauthClient { + http: reqwest::Client::new(), + client_id: id.clone(), + client_secret: secret.clone(), + redirect_uri: format!("{host}/auth/github/callback"), + user_agent: Default::default(), + scopes: Default::default(), + states: Default::default(), + } + .into() + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_login_redirect_url( + &self, + redirect: Option, + ) -> String { + let state_prefix = random_string(STATE_PREFIX_LENGTH); + let state = match redirect { + Some(redirect) => format!("{state_prefix}{redirect}"), + None => state_prefix, + }; + let redirect_url = format!( + "https://github.com/login/oauth/authorize?state={state}&client_id={}&redirect_uri={}&scope={}", + self.client_id, self.redirect_uri, self.scopes + ); + let mut states = self.states.lock().await; + states.push(state); + redirect_url + } + + #[instrument(level = "debug", skip(self))] + pub async fn check_state(&self, state: &str) -> bool { + let mut contained = false; + self.states.lock().await.retain(|s| { + if s.as_str() == state { + contained = true; + false + } else { + true + } + }); + contained + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_access_token( + &self, + code: &str, + ) -> anyhow::Result { + self + .post::<(), _>( + "https://github.com/login/oauth/access_token", + &[ + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ("redirect_uri", self.redirect_uri.as_str()), + ("code", code), + ], + None, + None, + ) + .await + .context("failed to get github access token using code") + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_github_user( + &self, + token: &str, + ) -> anyhow::Result { + self + .get("https://api.github.com/user", &[], Some(token)) + .await + .context("failed to get github user using access token") + } + + #[instrument(level = "debug", skip(self))] + async fn get( + &self, + endpoint: &str, + query: &[(&str, &str)], + bearer_token: Option<&str>, + ) -> anyhow::Result { + let mut req = self + .http + .get(endpoint) + .query(query) + .header("User-Agent", &self.user_agent); + + if let Some(bearer_token) = bearer_token { + req = + req.header("Authorization", format!("Bearer {bearer_token}")); + } + + let res = req.send().await.context("failed to reach github")?; + + let status = res.status(); + + if status == StatusCode::OK { + let body = res + .json() + .await + .context("failed to parse body into expected type")?; + Ok(body) + } else { + let text = res.text().await.context(format!( + "status: {status} | failed to get response text" + ))?; + Err(anyhow!("status: {status} | text: {text}")) + } + } + + async fn post( + &self, + endpoint: &str, + query: &[(&str, &str)], + body: Option<&B>, + bearer_token: Option<&str>, + ) -> anyhow::Result { + let mut req = self + .http + .post(endpoint) + .query(query) + .header("Accept", "application/json") + .header("User-Agent", &self.user_agent); + + if let Some(body) = body { + req = req.json(body); + } + + if let Some(bearer_token) = bearer_token { + req = + req.header("Authorization", format!("Bearer {bearer_token}")); + } + + let res = req.send().await.context("failed to reach github")?; + + let status = res.status(); + + if status == StatusCode::OK { + let body = res + .json() + .await + .context("failed to parse POST body into expected type")?; + Ok(body) + } else { + let text = res.text().await.with_context(|| format!( + "method: POST | status: {status} | failed to get response text" + ))?; + Err(anyhow!("method: POST | status: {status} | text: {text}")) + } + } +} + +#[derive(Deserialize)] +pub struct AccessTokenResponse { + pub access_token: String, + pub scope: String, + pub token_type: String, +} + +#[derive(Deserialize)] +pub struct GithubUserResponse { + pub login: String, + pub id: u128, + pub avatar_url: String, + pub email: Option, +} diff --git a/bin/core/src/auth/github/mod.rs b/bin/core/src/auth/github/mod.rs new file mode 100644 index 000000000..aec3f3896 --- /dev/null +++ b/bin/core/src/auth/github/mod.rs @@ -0,0 +1,118 @@ +use anyhow::{anyhow, Context}; +use axum::{ + extract::Query, response::Redirect, routing::get, Router, +}; +use monitor_client::entities::{ + monitor_timestamp, + user::{User, UserConfig}, +}; +use mungos::mongodb::bson::doc; +use reqwest::StatusCode; +use serde::Deserialize; +use serror::AddStatusCode; + +use crate::{ + config::core_config, + state::{db_client, jwt_client}, +}; + +use self::client::github_oauth_client; + +use super::{RedirectQuery, STATE_PREFIX_LENGTH}; + +pub mod client; + +pub fn router() -> Router { + Router::new() + .route( + "/login", + get(|Query(query): Query| async { + Redirect::to( + &github_oauth_client() + .as_ref() + // OK: the router is only mounted in case that the client is populated + .unwrap() + .get_login_redirect_url(query.redirect) + .await, + ) + }), + ) + .route( + "/callback", + get(|query| async { + callback(query).await.status_code(StatusCode::UNAUTHORIZED) + }), + ) +} + +#[derive(Debug, Deserialize)] +struct CallbackQuery { + state: String, + code: String, +} + +#[instrument(name = "GithubCallback", level = "debug")] +async fn callback( + Query(query): Query, +) -> anyhow::Result { + let client = github_oauth_client().as_ref().unwrap(); + if !client.check_state(&query.state).await { + return Err(anyhow!("state mismatch")); + } + let token = client.get_access_token(&query.code).await?; + let github_user = + client.get_github_user(&token.access_token).await?; + let github_id = github_user.id.to_string(); + let db_client = db_client().await; + let user = db_client + .users + .find_one(doc! { "config.data.github_id": &github_id }, None) + .await + .context("failed at find user query from mongo")?; + let jwt = match user { + Some(user) => jwt_client() + .generate(user.id) + .context("failed to generate jwt")?, + None => { + let ts = monitor_timestamp(); + let no_users_exist = + db_client.users.find_one(None, None).await?.is_none(); + let user = User { + id: Default::default(), + username: github_user.login, + enabled: no_users_exist, + admin: no_users_exist, + create_server_permissions: no_users_exist, + create_build_permissions: no_users_exist, + updated_at: ts, + last_update_view: 0, + recently_viewed: Vec::new(), + config: UserConfig::Github { + github_id, + avatar: github_user.avatar_url, + }, + }; + let user_id = db_client + .users + .insert_one(user, None) + .await + .context("failed to create user on mongo")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + jwt_client() + .generate(user_id) + .context("failed to generate jwt")? + } + }; + let exchange_token = jwt_client().create_exchange_token(jwt).await; + let redirect = &query.state[STATE_PREFIX_LENGTH..]; + let redirect_url = if redirect.is_empty() { + format!("{}?token={exchange_token}", core_config().host) + } else { + let splitter = if redirect.contains('?') { '&' } else { '?' }; + format!("{}{splitter}token={exchange_token}", redirect) + }; + Ok(Redirect::to(&redirect_url)) +} diff --git a/bin/core/src/auth/google/client.rs b/bin/core/src/auth/google/client.rs new file mode 100644 index 000000000..103522e39 --- /dev/null +++ b/bin/core/src/auth/google/client.rs @@ -0,0 +1,198 @@ +use std::sync::OnceLock; + +use anyhow::{anyhow, Context}; +use jwt::Token; +use monitor_client::entities::config::core::{CoreConfig, OauthCredentials}; +use reqwest::StatusCode; +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use tokio::sync::Mutex; + +use crate::{ + auth::{random_string, STATE_PREFIX_LENGTH}, + config::core_config, +}; + +pub fn google_oauth_client() -> &'static Option { + static GOOGLE_OAUTH_CLIENT: OnceLock> = + OnceLock::new(); + GOOGLE_OAUTH_CLIENT + .get_or_init(|| GoogleOauthClient::new(core_config())) +} + +pub struct GoogleOauthClient { + http: reqwest::Client, + client_id: String, + client_secret: String, + redirect_uri: String, + scopes: String, + states: Mutex>, + user_agent: String, +} + +impl GoogleOauthClient { + pub fn new( + CoreConfig { + google_oauth: + OauthCredentials { + enabled, + id, + secret, + }, + host, + .. + }: &CoreConfig, + ) -> Option { + if !enabled { + return None; + } + if host.is_empty() { + warn!("google oauth is enabled, but 'config.host' is not configured"); + return None; + } + if id.is_empty() { + warn!("google oauth is enabled, but 'config.google_oauth.id' is not configured"); + return None; + } + if secret.is_empty() { + warn!("google oauth is enabled, but 'config.google_oauth.secret' is not configured"); + return None; + } + let scopes = urlencoding::encode( + &[ + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + ] + .join(" "), + ) + .to_string(); + GoogleOauthClient { + http: Default::default(), + client_id: id.clone(), + client_secret: secret.clone(), + redirect_uri: format!("{host}/auth/google/callback"), + user_agent: String::from("monitor"), + states: Default::default(), + scopes, + } + .into() + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_login_redirect_url( + &self, + redirect: Option, + ) -> String { + let state_prefix = random_string(STATE_PREFIX_LENGTH); + let state = match redirect { + Some(redirect) => format!("{state_prefix}{redirect}"), + None => state_prefix, + }; + let redirect_url = format!( + "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}", + self.client_id, self.redirect_uri, self.scopes + ); + let mut states = self.states.lock().await; + states.push(state); + redirect_url + } + + #[instrument(level = "debug", skip(self))] + pub async fn check_state(&self, state: &str) -> bool { + let mut contained = false; + self.states.lock().await.retain(|s| { + if s.as_str() == state { + contained = true; + false + } else { + true + } + }); + contained + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_access_token( + &self, + code: &str, + ) -> anyhow::Result { + self + .post::<_>( + "https://oauth2.googleapis.com/token", + &[ + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ("redirect_uri", self.redirect_uri.as_str()), + ("code", code), + ("grant_type", "authorization_code"), + ], + None, + ) + .await + .context("failed to get google access token using code") + } + + #[instrument(level = "debug", skip(self))] + pub fn get_google_user( + &self, + id_token: &str, + ) -> anyhow::Result { + let t: Token = + Token::parse_unverified(id_token) + .context("failed to parse id_token")?; + Ok(t.claims().to_owned()) + } + + #[instrument(level = "debug", skip(self))] + async fn post( + &self, + endpoint: &str, + body: &[(&str, &str)], + bearer_token: Option<&str>, + ) -> anyhow::Result { + let mut req = self + .http + .post(endpoint) + .form(body) + .header("Accept", "application/json") + .header("User-Agent", &self.user_agent); + + if let Some(bearer_token) = bearer_token { + req = + req.header("Authorization", format!("Bearer {bearer_token}")); + } + + let res = req.send().await.context("failed to reach google")?; + + let status = res.status(); + + if status == StatusCode::OK { + let body = res + .json() + .await + .context("failed to parse POST body into expected type")?; + Ok(body) + } else { + let text = res.text().await.context(format!( + "method: POST | status: {status} | failed to get response text" + ))?; + Err(anyhow!("method: POST | status: {status} | text: {text}")) + } + } +} + +#[derive(Deserialize)] +pub struct AccessTokenResponse { + pub access_token: String, + pub id_token: String, + pub scope: String, + pub token_type: String, +} + +#[derive(Deserialize, Clone)] +pub struct GoogleUser { + #[serde(rename = "sub")] + pub id: String, + pub email: String, + pub picture: String, +} diff --git a/bin/core/src/auth/google/mod.rs b/bin/core/src/auth/google/mod.rs new file mode 100644 index 000000000..b14677c07 --- /dev/null +++ b/bin/core/src/auth/google/mod.rs @@ -0,0 +1,133 @@ +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use axum::{ + extract::Query, response::Redirect, routing::get, Router, +}; +use monitor_client::entities::user::{User, UserConfig}; +use mungos::mongodb::bson::doc; +use reqwest::StatusCode; +use serde::Deserialize; +use serror::AddStatusCode; + +use crate::{ + config::core_config, + state::{db_client, jwt_client}, +}; + +use self::client::google_oauth_client; + +use super::{RedirectQuery, STATE_PREFIX_LENGTH}; + +pub mod client; + +pub fn router() -> Router { + Router::new() + .route( + "/login", + get(|Query(query): Query| async move { + Redirect::to( + &google_oauth_client() + .as_ref() + // OK: its not mounted unless the client is populated + .unwrap() + .get_login_redirect_url(query.redirect) + .await, + ) + }), + ) + .route( + "/callback", + get(|query| async { + callback(query).await.status_code(StatusCode::UNAUTHORIZED) + }), + ) +} + +#[derive(Debug, Deserialize)] +struct CallbackQuery { + state: Option, + code: Option, + error: Option, +} + +#[instrument(name = "GoogleCallback", level = "debug")] +async fn callback( + Query(query): Query, +) -> anyhow::Result { + // Safe: the method is only called after the client is_some + let client = google_oauth_client().as_ref().unwrap(); + if let Some(error) = query.error { + return Err(anyhow!("auth error from google: {error}")); + } + let state = query + .state + .context("callback query does not contain state")?; + if !client.check_state(&state).await { + return Err(anyhow!("state mismatch")); + } + let token = client + .get_access_token( + &query.code.context("callback query does not contain code")?, + ) + .await?; + let google_user = client.get_google_user(&token.id_token)?; + let google_id = google_user.id.to_string(); + let db_client = db_client().await; + let user = db_client + .users + .find_one(doc! { "config.data.google_id": &google_id }, None) + .await + .context("failed at find user query from mongo")?; + let jwt = match user { + Some(user) => jwt_client() + .generate(user.id) + .context("failed to generate jwt")?, + None => { + let ts = unix_timestamp_ms() as i64; + let no_users_exist = + db_client.users.find_one(None, None).await?.is_none(); + let user = User { + id: Default::default(), + username: google_user + .email + .split('@') + .collect::>() + .first() + .unwrap() + .to_string(), + enabled: no_users_exist, + admin: no_users_exist, + create_server_permissions: no_users_exist, + create_build_permissions: no_users_exist, + updated_at: ts, + last_update_view: 0, + recently_viewed: Vec::new(), + config: UserConfig::Google { + google_id, + avatar: google_user.picture, + }, + }; + let user_id = db_client + .users + .insert_one(user, None) + .await + .context("failed to create user on mongo")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + jwt_client() + .generate(user_id) + .context("failed to generate jwt")? + } + }; + let exchange_token = jwt_client().create_exchange_token(jwt).await; + let redirect = &state[STATE_PREFIX_LENGTH..]; + let redirect_url = if redirect.is_empty() { + format!("{}?token={exchange_token}", core_config().host) + } else { + let splitter = if redirect.contains('?') { '&' } else { '?' }; + format!("{}{splitter}token={exchange_token}", redirect) + }; + Ok(Redirect::to(&redirect_url)) +} diff --git a/bin/core/src/auth/jwt.rs b/bin/core/src/auth/jwt.rs new file mode 100644 index 000000000..2e2a7b937 --- /dev/null +++ b/bin/core/src/auth/jwt.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, Context}; +use async_timing_util::{ + get_timelength_in_ms, unix_timestamp_ms, Timelength, +}; +use hmac::{Hmac, Mac}; +use jwt::SignWithKey; +use monitor_client::entities::config::core::CoreConfig; +use mungos::mongodb::bson::doc; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use tokio::sync::Mutex; + +use super::random_string; + +type ExchangeTokenMap = Mutex>; + +#[derive(Serialize, Deserialize)] +pub struct JwtClaims { + pub id: String, + pub iat: u128, + pub exp: u128, +} + +pub struct JwtClient { + pub key: Hmac, + valid_for_ms: u128, + exchange_tokens: ExchangeTokenMap, +} + +impl JwtClient { + pub fn new(config: &CoreConfig) -> JwtClient { + let key = Hmac::new_from_slice(random_string(40).as_bytes()) + .expect("failed at taking HmacSha256 of jwt secret"); + JwtClient { + key, + valid_for_ms: get_timelength_in_ms( + config.jwt_valid_for.to_string().parse().unwrap(), + ), + exchange_tokens: Default::default(), + } + } + + pub fn generate(&self, user_id: String) -> anyhow::Result { + let iat = unix_timestamp_ms(); + let exp = iat + self.valid_for_ms; + let claims = JwtClaims { + id: user_id, + iat, + exp, + }; + let jwt = claims + .sign_with_key(&self.key) + .context("failed at signing claim")?; + Ok(jwt) + } + + #[instrument(level = "debug", skip_all)] + pub async fn create_exchange_token(&self, jwt: String) -> String { + let exchange_token = random_string(40); + self.exchange_tokens.lock().await.insert( + exchange_token.clone(), + ( + jwt, + unix_timestamp_ms() + + get_timelength_in_ms(Timelength::OneMinute), + ), + ); + exchange_token + } + #[instrument(level = "debug", skip(self))] + pub async fn redeem_exchange_token( + &self, + exchange_token: &str, + ) -> anyhow::Result { + let (jwt, valid_until) = self + .exchange_tokens + .lock() + .await + .remove(exchange_token) + .context("invalid exchange token: unrecognized")?; + if unix_timestamp_ms() < valid_until { + Ok(jwt) + } else { + Err(anyhow!("invalid exchange token: expired")) + } + } +} diff --git a/bin/core/src/auth/local.rs b/bin/core/src/auth/local.rs new file mode 100644 index 000000000..3cd535f6d --- /dev/null +++ b/bin/core/src/auth/local.rs @@ -0,0 +1,133 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use async_trait::async_trait; +use axum::http::HeaderMap; +use monitor_client::{ + api::auth::{ + CreateLocalUser, CreateLocalUserResponse, LoginLocalUser, + LoginLocalUserResponse, + }, + entities::user::{User, UserConfig}, +}; +use mungos::mongodb::bson::{doc, oid::ObjectId}; +use resolver_api::Resolve; + +use crate::{ + config::core_config, + state::State, + state::{db_client, jwt_client}, +}; + +const BCRYPT_COST: u32 = 10; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateLocalUser", skip(self))] + async fn resolve( + &self, + CreateLocalUser { username, password }: CreateLocalUser, + _: HeaderMap, + ) -> anyhow::Result { + if !core_config().local_auth { + return Err(anyhow!("local auth is not enabled")); + } + + if username.is_empty() { + return Err(anyhow!("username cannot be empty string")); + } + + if ObjectId::from_str(&username).is_ok() { + return Err(anyhow!("username cannot be valid ObjectId")); + } + + let password = bcrypt::hash(password, BCRYPT_COST) + .context("failed to hash password")?; + + let no_users_exist = db_client() + .await + .users + .find_one(None, None) + .await? + .is_none(); + + let ts = unix_timestamp_ms() as i64; + + let user = User { + id: Default::default(), + username, + enabled: no_users_exist, + admin: no_users_exist, + create_server_permissions: no_users_exist, + create_build_permissions: no_users_exist, + updated_at: ts, + last_update_view: 0, + recently_viewed: Vec::new(), + config: UserConfig::Local { password }, + }; + + let user_id = db_client() + .await + .users + .insert_one(user, None) + .await + .context("failed to create user")? + .inserted_id + .as_object_id() + .context("inserted_id is not ObjectId")? + .to_string(); + + let jwt = jwt_client() + .generate(user_id) + .context("failed to generate jwt for user")?; + + Ok(CreateLocalUserResponse { jwt }) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "LoginLocalUser", level = "debug", skip(self))] + async fn resolve( + &self, + LoginLocalUser { username, password }: LoginLocalUser, + _: HeaderMap, + ) -> anyhow::Result { + if !core_config().local_auth { + return Err(anyhow!("local auth is not enabled")); + } + + let user = db_client() + .await + .users + .find_one(doc! { "username": &username }, None) + .await + .context("failed at db query for users")? + .with_context(|| { + format!("did not find user with username {username}") + })?; + + let UserConfig::Local { + password: user_pw_hash, + } = user.config + else { + return Err(anyhow!( + "non-local auth users can not log in with a password" + )); + }; + + let verified = bcrypt::verify(password, &user_pw_hash) + .context("failed at verify password")?; + + if !verified { + return Err(anyhow!("invalid credentials")); + } + + let jwt = jwt_client() + .generate(user.id) + .context("failed at generating jwt for user")?; + + Ok(LoginLocalUserResponse { jwt }) + } +} diff --git a/bin/core/src/auth/mod.rs b/bin/core/src/auth/mod.rs new file mode 100644 index 000000000..61349864d --- /dev/null +++ b/bin/core/src/auth/mod.rs @@ -0,0 +1,161 @@ +use ::jwt::VerifyWithKey; +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use axum::{ + extract::Request, http::HeaderMap, middleware::Next, + response::Response, +}; +use monitor_client::entities::{monitor_timestamp, user::User}; +use mungos::mongodb::bson::doc; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::Deserialize; + +use crate::{ + helpers::query::get_user, + state::{db_client, jwt_client}, +}; + +use self::jwt::JwtClaims; + +pub mod github; +pub mod google; +pub mod jwt; + +mod local; + +const STATE_PREFIX_LENGTH: usize = 20; + +#[derive(Deserialize)] +pub struct RedirectQuery { + pub redirect: Option, +} + +#[instrument(level = "debug")] +pub async fn auth_request( + headers: HeaderMap, + mut req: Request, + next: Next, +) -> serror::Result { + let user = authenticate_check_enabled(&headers).await?; + req.extensions_mut().insert(user); + Ok(next.run(req).await) +} + +pub fn random_string(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +#[instrument(level = "debug")] +pub async fn get_user_id_from_headers( + headers: &HeaderMap, +) -> anyhow::Result { + match ( + headers.get("authorization"), + headers.get("x-api-key"), + headers.get("x-api-secret"), + ) { + (Some(jwt), _, _) => { + // USE JWT + let jwt = jwt.to_str().context("jwt is not str")?; + auth_jwt_get_user_id(jwt) + .await + .context("failed to authenticate jwt") + } + (None, Some(key), Some(secret)) => { + // USE API KEY / SECRET + let key = key.to_str().context("key is not str")?; + let secret = secret.to_str().context("secret is not str")?; + auth_api_key_get_user_id(key, secret) + .await + .context("failed to authenticate api key") + } + _ => { + // AUTH FAIL + Err(anyhow!("must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET")) + } + } +} + +#[instrument(level = "debug")] +pub async fn authenticate_check_enabled( + headers: &HeaderMap, +) -> anyhow::Result { + let user_id = get_user_id_from_headers(headers).await?; + let user = get_user(&user_id).await?; + if user.enabled { + Ok(user) + } else { + Err(anyhow!("user not enabled")) + } +} + +#[instrument(level = "debug")] +pub async fn auth_jwt_get_user_id( + jwt: &str, +) -> anyhow::Result { + let claims: JwtClaims = jwt + .verify_with_key(&jwt_client().key) + .context("failed to verify claims")?; + if claims.exp > unix_timestamp_ms() { + Ok(claims.id) + } else { + Err(anyhow!("token has expired")) + } +} + +#[instrument(level = "debug")] +pub async fn auth_jwt_check_enabled( + jwt: &str, +) -> anyhow::Result { + let user_id = auth_jwt_get_user_id(jwt).await?; + check_enabled(user_id).await +} + +#[instrument(level = "debug")] +pub async fn auth_api_key_get_user_id( + key: &str, + secret: &str, +) -> anyhow::Result { + let key = db_client() + .await + .api_keys + .find_one(doc! { "key": key }, None) + .await + .context("failed to query db")? + .context("no api key matching key")?; + if key.expires != 0 && key.expires < monitor_timestamp() { + return Err(anyhow!("api key expired")); + } + if bcrypt::verify(secret, &key.secret) + .context("failed to verify secret hash")? + { + // secret matches + Ok(key.user_id) + } else { + // secret mismatch + Err(anyhow!("invalid api secret")) + } +} + +#[instrument(level = "debug")] +pub async fn auth_api_key_check_enabled( + key: &str, + secret: &str, +) -> anyhow::Result { + let user_id = auth_api_key_get_user_id(key, secret).await?; + check_enabled(user_id).await +} + +#[instrument(level = "debug")] +async fn check_enabled(user_id: String) -> anyhow::Result { + let user = get_user(&user_id).await?; + if user.enabled { + Ok(user) + } else { + Err(anyhow!("user not enabled")) + } +} diff --git a/bin/core/src/cloud/aws.rs b/bin/core/src/cloud/aws.rs new file mode 100644 index 000000000..7f3782cf9 --- /dev/null +++ b/bin/core/src/cloud/aws.rs @@ -0,0 +1,1056 @@ +use std::{str::FromStr, time::Duration}; + +use anyhow::{anyhow, Context}; +use aws_config::BehaviorVersion; +use aws_sdk_ec2::{ + config::Region, + types::{ + BlockDeviceMapping, EbsBlockDevice, + InstanceNetworkInterfaceSpecification, InstanceStateChange, + InstanceStateName, InstanceStatus, InstanceType, ResourceType, + Tag, TagSpecification, VolumeType, + }, + Client, +}; +use monitor_client::entities::{ + alert::{Alert, AlertData, AlertDataVariant}, + monitor_timestamp, + server::stats::SeverityLevel, + server_template::AwsServerTemplateConfig, + update::ResourceTarget, +}; + +use crate::{config::core_config, helpers::alert::send_alerts}; + +const POLL_RATE_SECS: u64 = 2; +const MAX_POLL_TRIES: usize = 30; + +pub struct Ec2Instance { + pub instance_id: String, + pub ip: String, +} + +#[instrument] +async fn create_ec2_client(region: String) -> Client { + // There may be a better way to pass these keys to client + std::env::set_var( + "AWS_ACCESS_KEY_ID", + &core_config().aws.access_key_id, + ); + std::env::set_var( + "AWS_SECRET_ACCESS_KEY", + &core_config().aws.secret_access_key, + ); + let region = Region::new(region); + let config = aws_config::defaults(BehaviorVersion::v2023_11_09()) + .region(region) + .load() + .await; + Client::new(&config) +} + +#[instrument] +pub async fn launch_ec2_instance( + name: &str, + config: AwsServerTemplateConfig, +) -> anyhow::Result { + let AwsServerTemplateConfig { + region, + instance_type, + volumes, + ami_id, + subnet_id, + security_group_ids, + key_pair_name, + assign_public_ip, + use_public_ip, + port: _, + } = config; + let instance_type = handle_unknown_instance_type( + InstanceType::from(instance_type.as_str()), + )?; + let client = create_ec2_client(region.clone()).await; + let mut req = client + .run_instances() + .image_id(ami_id) + .instance_type(instance_type) + .network_interfaces( + InstanceNetworkInterfaceSpecification::builder() + .subnet_id(subnet_id) + .associate_public_ip_address(assign_public_ip) + .set_groups(security_group_ids.to_vec().into()) + .device_index(0) + .build(), + ) + .key_name(key_pair_name) + .tag_specifications( + TagSpecification::builder() + .tags(Tag::builder().key("Name").value(name).build()) + .resource_type(ResourceType::Instance) + .build(), + ) + .min_count(1) + .max_count(1); + + for volume in volumes { + let mut ebs = EbsBlockDevice::builder() + .volume_size(volume.size_gb) + .set_iops(volume.iops) + .set_throughput(volume.throughput); + if let Some(volume_type) = &volume.volume_type { + ebs = ebs.volume_type( + VolumeType::from_str(volume_type) + .context("invalid volume type")?, + ); + } + req = req.block_device_mappings( + BlockDeviceMapping::builder() + .set_device_name(volume.device_name.clone().into()) + .set_ebs(ebs.build().into()) + .build(), + ) + } + + let res = req + .send() + .await + .context("failed to start builder ec2 instance")?; + + let instance = res + .instances() + .first() + .context("instances array is empty")?; + + let instance_id = instance + .instance_id() + .context("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 use_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() + }; + return Ok(Ec2Instance { instance_id, ip }); + } + tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await; + } + Err(anyhow!("instance not running after polling")) +} + +const MAX_TERMINATION_TRIES: usize = 5; +const TERMINATION_WAIT_SECS: u64 = 15; + +#[instrument] +pub async fn terminate_ec2_instance_with_retry( + region: String, + instance_id: &str, +) -> anyhow::Result { + let client = create_ec2_client(region).await; + for i in 0..MAX_TERMINATION_TRIES { + match terminate_ec2_instance_inner(&client, instance_id).await { + Ok(res) => { + info!("instance {instance_id} successfully terminated."); + return Ok(res); + } + Err(e) => { + if i == MAX_TERMINATION_TRIES - 1 { + error!("failed to terminate instance {instance_id}."); + let alert = Alert { + id: Default::default(), + ts: monitor_timestamp(), + resolved: false, + level: SeverityLevel::Critical, + target: ResourceTarget::system(), + variant: AlertDataVariant::AwsBuilderTerminationFailed, + data: AlertData::AwsBuilderTerminationFailed { + instance_id: instance_id.to_string(), + }, + resolved_ts: None, + }; + send_alerts(&[alert]).await; + return Err(e); + } + tokio::time::sleep(Duration::from_secs( + TERMINATION_WAIT_SECS, + )) + .await; + } + } + } + unreachable!() +} + +#[instrument] +async fn terminate_ec2_instance_inner( + client: &Client, + instance_id: &str, +) -> anyhow::Result { + let res = client + .terminate_instances() + .instance_ids(instance_id) + .send() + .await + .context("failed to terminate instance from aws")? + .terminating_instances() + .first() + .context("terminating instances is empty")? + .to_owned(); + Ok(res) +} + +#[instrument(level = "debug")] +async fn get_ec2_instance_status( + client: &Client, + instance_id: &str, +) -> anyhow::Result> { + let status = client + .describe_instance_status() + .instance_ids(instance_id) + .send() + .await + .context("failed to get instance status from aws")? + .instance_statuses() + .first() + .cloned(); + Ok(status) +} + +#[instrument(level = "debug")] +async fn get_ec2_instance_state_name( + client: &Client, + instance_id: &str, +) -> anyhow::Result> { + 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)) +} + +#[instrument(level = "debug")] +async fn get_ec2_instance_public_ip( + client: &Client, + instance_id: &str, +) -> anyhow::Result { + let ip = client + .describe_instances() + .instance_ids(instance_id) + .send() + .await + .context("failed to get instance status from aws")? + .reservations() + .first() + .context("instance reservations is empty")? + .instances() + .first() + .context("instances is empty")? + .public_ip_address() + .context("instance has no public ip")? + .to_string(); + + Ok(ip) +} + +fn handle_unknown_instance_type( + instance_type: InstanceType, +) -> anyhow::Result { + match instance_type { + InstanceType::A12xlarge + | InstanceType::A14xlarge + | InstanceType::A1Large + | InstanceType::A1Medium + | InstanceType::A1Metal + | InstanceType::A1Xlarge + | InstanceType::C1Medium + | InstanceType::C1Xlarge + | InstanceType::C32xlarge + | InstanceType::C34xlarge + | InstanceType::C38xlarge + | InstanceType::C3Large + | InstanceType::C3Xlarge + | InstanceType::C42xlarge + | InstanceType::C44xlarge + | InstanceType::C48xlarge + | InstanceType::C4Large + | InstanceType::C4Xlarge + | InstanceType::C512xlarge + | InstanceType::C518xlarge + | InstanceType::C524xlarge + | InstanceType::C52xlarge + | InstanceType::C54xlarge + | InstanceType::C59xlarge + | InstanceType::C5Large + | InstanceType::C5Metal + | InstanceType::C5Xlarge + | InstanceType::C5a12xlarge + | InstanceType::C5a16xlarge + | InstanceType::C5a24xlarge + | InstanceType::C5a2xlarge + | InstanceType::C5a4xlarge + | InstanceType::C5a8xlarge + | InstanceType::C5aLarge + | InstanceType::C5aXlarge + | InstanceType::C5ad12xlarge + | InstanceType::C5ad16xlarge + | InstanceType::C5ad24xlarge + | InstanceType::C5ad2xlarge + | InstanceType::C5ad4xlarge + | InstanceType::C5ad8xlarge + | InstanceType::C5adLarge + | InstanceType::C5adXlarge + | InstanceType::C5d12xlarge + | InstanceType::C5d18xlarge + | InstanceType::C5d24xlarge + | InstanceType::C5d2xlarge + | InstanceType::C5d4xlarge + | InstanceType::C5d9xlarge + | InstanceType::C5dLarge + | InstanceType::C5dMetal + | InstanceType::C5dXlarge + | InstanceType::C5n18xlarge + | InstanceType::C5n2xlarge + | InstanceType::C5n4xlarge + | InstanceType::C5n9xlarge + | InstanceType::C5nLarge + | InstanceType::C5nMetal + | InstanceType::C5nXlarge + | InstanceType::C6a12xlarge + | InstanceType::C6a16xlarge + | InstanceType::C6a24xlarge + | InstanceType::C6a2xlarge + | InstanceType::C6a32xlarge + | InstanceType::C6a48xlarge + | InstanceType::C6a4xlarge + | InstanceType::C6a8xlarge + | InstanceType::C6aLarge + | InstanceType::C6aMetal + | InstanceType::C6aXlarge + | InstanceType::C6g12xlarge + | InstanceType::C6g16xlarge + | InstanceType::C6g2xlarge + | InstanceType::C6g4xlarge + | InstanceType::C6g8xlarge + | InstanceType::C6gLarge + | InstanceType::C6gMedium + | InstanceType::C6gMetal + | InstanceType::C6gXlarge + | InstanceType::C6gd12xlarge + | InstanceType::C6gd16xlarge + | InstanceType::C6gd2xlarge + | InstanceType::C6gd4xlarge + | InstanceType::C6gd8xlarge + | InstanceType::C6gdLarge + | InstanceType::C6gdMedium + | InstanceType::C6gdMetal + | InstanceType::C6gdXlarge + | InstanceType::C6gn12xlarge + | InstanceType::C6gn16xlarge + | InstanceType::C6gn2xlarge + | InstanceType::C6gn4xlarge + | InstanceType::C6gn8xlarge + | InstanceType::C6gnLarge + | InstanceType::C6gnMedium + | InstanceType::C6gnXlarge + | InstanceType::C6i12xlarge + | InstanceType::C6i16xlarge + | InstanceType::C6i24xlarge + | InstanceType::C6i2xlarge + | InstanceType::C6i32xlarge + | InstanceType::C6i4xlarge + | InstanceType::C6i8xlarge + | InstanceType::C6iLarge + | InstanceType::C6iMetal + | InstanceType::C6iXlarge + | InstanceType::C6id12xlarge + | InstanceType::C6id16xlarge + | InstanceType::C6id24xlarge + | InstanceType::C6id2xlarge + | InstanceType::C6id32xlarge + | InstanceType::C6id4xlarge + | InstanceType::C6id8xlarge + | InstanceType::C6idLarge + | InstanceType::C6idMetal + | InstanceType::C6idXlarge + | InstanceType::C6in12xlarge + | InstanceType::C6in16xlarge + | InstanceType::C6in24xlarge + | InstanceType::C6in2xlarge + | InstanceType::C6in32xlarge + | InstanceType::C6in4xlarge + | InstanceType::C6in8xlarge + | InstanceType::C6inLarge + | InstanceType::C6inMetal + | InstanceType::C6inXlarge + | InstanceType::C7a12xlarge + | InstanceType::C7a16xlarge + | InstanceType::C7a24xlarge + | InstanceType::C7a2xlarge + | InstanceType::C7a32xlarge + | InstanceType::C7a48xlarge + | InstanceType::C7a4xlarge + | InstanceType::C7a8xlarge + | InstanceType::C7aLarge + | InstanceType::C7aMedium + | InstanceType::C7aMetal48xl + | InstanceType::C7aXlarge + | InstanceType::C7g12xlarge + | InstanceType::C7g16xlarge + | InstanceType::C7g2xlarge + | InstanceType::C7g4xlarge + | InstanceType::C7g8xlarge + | InstanceType::C7gLarge + | InstanceType::C7gMedium + | InstanceType::C7gMetal + | InstanceType::C7gXlarge + | InstanceType::C7gd12xlarge + | InstanceType::C7gd16xlarge + | InstanceType::C7gd2xlarge + | InstanceType::C7gd4xlarge + | InstanceType::C7gd8xlarge + | InstanceType::C7gdLarge + | InstanceType::C7gdMedium + | InstanceType::C7gdXlarge + | InstanceType::C7gn12xlarge + | InstanceType::C7gn16xlarge + | InstanceType::C7gn2xlarge + | InstanceType::C7gn4xlarge + | InstanceType::C7gn8xlarge + | InstanceType::C7gnLarge + | InstanceType::C7gnMedium + | InstanceType::C7gnXlarge + | InstanceType::C7i12xlarge + | InstanceType::C7i16xlarge + | InstanceType::C7i24xlarge + | InstanceType::C7i2xlarge + | InstanceType::C7i48xlarge + | InstanceType::C7i4xlarge + | InstanceType::C7i8xlarge + | InstanceType::C7iLarge + | InstanceType::C7iMetal24xl + | InstanceType::C7iMetal48xl + | InstanceType::C7iXlarge + | InstanceType::Cc14xlarge + | InstanceType::Cc28xlarge + | InstanceType::Cg14xlarge + | InstanceType::Cr18xlarge + | InstanceType::D22xlarge + | InstanceType::D24xlarge + | InstanceType::D28xlarge + | InstanceType::D2Xlarge + | InstanceType::D32xlarge + | InstanceType::D34xlarge + | InstanceType::D38xlarge + | InstanceType::D3Xlarge + | InstanceType::D3en12xlarge + | InstanceType::D3en2xlarge + | InstanceType::D3en4xlarge + | InstanceType::D3en6xlarge + | InstanceType::D3en8xlarge + | InstanceType::D3enXlarge + | InstanceType::Dl124xlarge + | InstanceType::Dl2q24xlarge + | InstanceType::F116xlarge + | InstanceType::F12xlarge + | InstanceType::F14xlarge + | InstanceType::G22xlarge + | InstanceType::G28xlarge + | InstanceType::G316xlarge + | InstanceType::G34xlarge + | InstanceType::G38xlarge + | InstanceType::G3sXlarge + | InstanceType::G4ad16xlarge + | InstanceType::G4ad2xlarge + | InstanceType::G4ad4xlarge + | InstanceType::G4ad8xlarge + | InstanceType::G4adXlarge + | InstanceType::G4dn12xlarge + | InstanceType::G4dn16xlarge + | InstanceType::G4dn2xlarge + | InstanceType::G4dn4xlarge + | InstanceType::G4dn8xlarge + | InstanceType::G4dnMetal + | InstanceType::G4dnXlarge + | InstanceType::G512xlarge + | InstanceType::G516xlarge + | InstanceType::G524xlarge + | InstanceType::G52xlarge + | InstanceType::G548xlarge + | InstanceType::G54xlarge + | InstanceType::G58xlarge + | InstanceType::G5Xlarge + | InstanceType::G5g16xlarge + | InstanceType::G5g2xlarge + | InstanceType::G5g4xlarge + | InstanceType::G5g8xlarge + | InstanceType::G5gMetal + | InstanceType::G5gXlarge + | InstanceType::H116xlarge + | InstanceType::H12xlarge + | InstanceType::H14xlarge + | InstanceType::H18xlarge + | InstanceType::Hi14xlarge + | InstanceType::Hpc6a48xlarge + | InstanceType::Hpc6id32xlarge + | InstanceType::Hpc7a12xlarge + | InstanceType::Hpc7a24xlarge + | InstanceType::Hpc7a48xlarge + | InstanceType::Hpc7a96xlarge + | InstanceType::Hpc7g16xlarge + | InstanceType::Hpc7g4xlarge + | InstanceType::Hpc7g8xlarge + | InstanceType::Hs18xlarge + | InstanceType::I22xlarge + | InstanceType::I24xlarge + | InstanceType::I28xlarge + | InstanceType::I2Xlarge + | InstanceType::I316xlarge + | InstanceType::I32xlarge + | InstanceType::I34xlarge + | InstanceType::I38xlarge + | InstanceType::I3Large + | InstanceType::I3Metal + | InstanceType::I3Xlarge + | InstanceType::I3en12xlarge + | InstanceType::I3en24xlarge + | InstanceType::I3en2xlarge + | InstanceType::I3en3xlarge + | InstanceType::I3en6xlarge + | InstanceType::I3enLarge + | InstanceType::I3enMetal + | InstanceType::I3enXlarge + | InstanceType::I4g16xlarge + | InstanceType::I4g2xlarge + | InstanceType::I4g4xlarge + | InstanceType::I4g8xlarge + | InstanceType::I4gLarge + | InstanceType::I4gXlarge + | InstanceType::I4i12xlarge + | InstanceType::I4i16xlarge + | InstanceType::I4i24xlarge + | InstanceType::I4i2xlarge + | InstanceType::I4i32xlarge + | InstanceType::I4i4xlarge + | InstanceType::I4i8xlarge + | InstanceType::I4iLarge + | InstanceType::I4iMetal + | InstanceType::I4iXlarge + | InstanceType::Im4gn16xlarge + | InstanceType::Im4gn2xlarge + | InstanceType::Im4gn4xlarge + | InstanceType::Im4gn8xlarge + | InstanceType::Im4gnLarge + | InstanceType::Im4gnXlarge + | InstanceType::Inf124xlarge + | InstanceType::Inf12xlarge + | InstanceType::Inf16xlarge + | InstanceType::Inf1Xlarge + | InstanceType::Inf224xlarge + | InstanceType::Inf248xlarge + | InstanceType::Inf28xlarge + | InstanceType::Inf2Xlarge + | InstanceType::Is4gen2xlarge + | InstanceType::Is4gen4xlarge + | InstanceType::Is4gen8xlarge + | InstanceType::Is4genLarge + | InstanceType::Is4genMedium + | InstanceType::Is4genXlarge + | InstanceType::M1Large + | InstanceType::M1Medium + | InstanceType::M1Small + | InstanceType::M1Xlarge + | InstanceType::M22xlarge + | InstanceType::M24xlarge + | InstanceType::M2Xlarge + | InstanceType::M32xlarge + | InstanceType::M3Large + | InstanceType::M3Medium + | InstanceType::M3Xlarge + | InstanceType::M410xlarge + | InstanceType::M416xlarge + | InstanceType::M42xlarge + | InstanceType::M44xlarge + | InstanceType::M4Large + | InstanceType::M4Xlarge + | InstanceType::M512xlarge + | InstanceType::M516xlarge + | InstanceType::M524xlarge + | InstanceType::M52xlarge + | InstanceType::M54xlarge + | InstanceType::M58xlarge + | InstanceType::M5Large + | InstanceType::M5Metal + | InstanceType::M5Xlarge + | InstanceType::M5a12xlarge + | InstanceType::M5a16xlarge + | InstanceType::M5a24xlarge + | InstanceType::M5a2xlarge + | InstanceType::M5a4xlarge + | InstanceType::M5a8xlarge + | InstanceType::M5aLarge + | InstanceType::M5aXlarge + | InstanceType::M5ad12xlarge + | InstanceType::M5ad16xlarge + | InstanceType::M5ad24xlarge + | InstanceType::M5ad2xlarge + | InstanceType::M5ad4xlarge + | InstanceType::M5ad8xlarge + | InstanceType::M5adLarge + | InstanceType::M5adXlarge + | InstanceType::M5d12xlarge + | InstanceType::M5d16xlarge + | InstanceType::M5d24xlarge + | InstanceType::M5d2xlarge + | InstanceType::M5d4xlarge + | InstanceType::M5d8xlarge + | InstanceType::M5dLarge + | InstanceType::M5dMetal + | InstanceType::M5dXlarge + | InstanceType::M5dn12xlarge + | InstanceType::M5dn16xlarge + | InstanceType::M5dn24xlarge + | InstanceType::M5dn2xlarge + | InstanceType::M5dn4xlarge + | InstanceType::M5dn8xlarge + | InstanceType::M5dnLarge + | InstanceType::M5dnMetal + | InstanceType::M5dnXlarge + | InstanceType::M5n12xlarge + | InstanceType::M5n16xlarge + | InstanceType::M5n24xlarge + | InstanceType::M5n2xlarge + | InstanceType::M5n4xlarge + | InstanceType::M5n8xlarge + | InstanceType::M5nLarge + | InstanceType::M5nMetal + | InstanceType::M5nXlarge + | InstanceType::M5zn12xlarge + | InstanceType::M5zn2xlarge + | InstanceType::M5zn3xlarge + | InstanceType::M5zn6xlarge + | InstanceType::M5znLarge + | InstanceType::M5znMetal + | InstanceType::M5znXlarge + | InstanceType::M6a12xlarge + | InstanceType::M6a16xlarge + | InstanceType::M6a24xlarge + | InstanceType::M6a2xlarge + | InstanceType::M6a32xlarge + | InstanceType::M6a48xlarge + | InstanceType::M6a4xlarge + | InstanceType::M6a8xlarge + | InstanceType::M6aLarge + | InstanceType::M6aMetal + | InstanceType::M6aXlarge + | InstanceType::M6g12xlarge + | InstanceType::M6g16xlarge + | InstanceType::M6g2xlarge + | InstanceType::M6g4xlarge + | InstanceType::M6g8xlarge + | InstanceType::M6gLarge + | InstanceType::M6gMedium + | InstanceType::M6gMetal + | InstanceType::M6gXlarge + | InstanceType::M6gd12xlarge + | InstanceType::M6gd16xlarge + | InstanceType::M6gd2xlarge + | InstanceType::M6gd4xlarge + | InstanceType::M6gd8xlarge + | InstanceType::M6gdLarge + | InstanceType::M6gdMedium + | InstanceType::M6gdMetal + | InstanceType::M6gdXlarge + | InstanceType::M6i12xlarge + | InstanceType::M6i16xlarge + | InstanceType::M6i24xlarge + | InstanceType::M6i2xlarge + | InstanceType::M6i32xlarge + | InstanceType::M6i4xlarge + | InstanceType::M6i8xlarge + | InstanceType::M6iLarge + | InstanceType::M6iMetal + | InstanceType::M6iXlarge + | InstanceType::M6id12xlarge + | InstanceType::M6id16xlarge + | InstanceType::M6id24xlarge + | InstanceType::M6id2xlarge + | InstanceType::M6id32xlarge + | InstanceType::M6id4xlarge + | InstanceType::M6id8xlarge + | InstanceType::M6idLarge + | InstanceType::M6idMetal + | InstanceType::M6idXlarge + | InstanceType::M6idn12xlarge + | InstanceType::M6idn16xlarge + | InstanceType::M6idn24xlarge + | InstanceType::M6idn2xlarge + | InstanceType::M6idn32xlarge + | InstanceType::M6idn4xlarge + | InstanceType::M6idn8xlarge + | InstanceType::M6idnLarge + | InstanceType::M6idnMetal + | InstanceType::M6idnXlarge + | InstanceType::M6in12xlarge + | InstanceType::M6in16xlarge + | InstanceType::M6in24xlarge + | InstanceType::M6in2xlarge + | InstanceType::M6in32xlarge + | InstanceType::M6in4xlarge + | InstanceType::M6in8xlarge + | InstanceType::M6inLarge + | InstanceType::M6inMetal + | InstanceType::M6inXlarge + | InstanceType::M7a12xlarge + | InstanceType::M7a16xlarge + | InstanceType::M7a24xlarge + | InstanceType::M7a2xlarge + | InstanceType::M7a32xlarge + | InstanceType::M7a48xlarge + | InstanceType::M7a4xlarge + | InstanceType::M7a8xlarge + | InstanceType::M7aLarge + | InstanceType::M7aMedium + | InstanceType::M7aMetal48xl + | InstanceType::M7aXlarge + | InstanceType::M7g12xlarge + | InstanceType::M7g16xlarge + | InstanceType::M7g2xlarge + | InstanceType::M7g4xlarge + | InstanceType::M7g8xlarge + | InstanceType::M7gLarge + | InstanceType::M7gMedium + | InstanceType::M7gMetal + | InstanceType::M7gXlarge + | InstanceType::M7gd12xlarge + | InstanceType::M7gd16xlarge + | InstanceType::M7gd2xlarge + | InstanceType::M7gd4xlarge + | InstanceType::M7gd8xlarge + | InstanceType::M7gdLarge + | InstanceType::M7gdMedium + | InstanceType::M7gdXlarge + | InstanceType::M7iFlex2xlarge + | InstanceType::M7iFlex4xlarge + | InstanceType::M7iFlex8xlarge + | InstanceType::M7iFlexLarge + | InstanceType::M7iFlexXlarge + | InstanceType::M7i12xlarge + | InstanceType::M7i16xlarge + | InstanceType::M7i24xlarge + | InstanceType::M7i2xlarge + | InstanceType::M7i48xlarge + | InstanceType::M7i4xlarge + | InstanceType::M7i8xlarge + | InstanceType::M7iLarge + | InstanceType::M7iMetal24xl + | InstanceType::M7iMetal48xl + | InstanceType::M7iXlarge + | InstanceType::Mac1Metal + | InstanceType::Mac2M2Metal + | InstanceType::Mac2M2proMetal + | InstanceType::Mac2Metal + | InstanceType::P216xlarge + | InstanceType::P28xlarge + | InstanceType::P2Xlarge + | InstanceType::P316xlarge + | InstanceType::P32xlarge + | InstanceType::P38xlarge + | InstanceType::P3dn24xlarge + | InstanceType::P4d24xlarge + | InstanceType::P4de24xlarge + | InstanceType::P548xlarge + | InstanceType::R32xlarge + | InstanceType::R34xlarge + | InstanceType::R38xlarge + | InstanceType::R3Large + | InstanceType::R3Xlarge + | InstanceType::R416xlarge + | InstanceType::R42xlarge + | InstanceType::R44xlarge + | InstanceType::R48xlarge + | InstanceType::R4Large + | InstanceType::R4Xlarge + | InstanceType::R512xlarge + | InstanceType::R516xlarge + | InstanceType::R524xlarge + | InstanceType::R52xlarge + | InstanceType::R54xlarge + | InstanceType::R58xlarge + | InstanceType::R5Large + | InstanceType::R5Metal + | InstanceType::R5Xlarge + | InstanceType::R5a12xlarge + | InstanceType::R5a16xlarge + | InstanceType::R5a24xlarge + | InstanceType::R5a2xlarge + | InstanceType::R5a4xlarge + | InstanceType::R5a8xlarge + | InstanceType::R5aLarge + | InstanceType::R5aXlarge + | InstanceType::R5ad12xlarge + | InstanceType::R5ad16xlarge + | InstanceType::R5ad24xlarge + | InstanceType::R5ad2xlarge + | InstanceType::R5ad4xlarge + | InstanceType::R5ad8xlarge + | InstanceType::R5adLarge + | InstanceType::R5adXlarge + | InstanceType::R5b12xlarge + | InstanceType::R5b16xlarge + | InstanceType::R5b24xlarge + | InstanceType::R5b2xlarge + | InstanceType::R5b4xlarge + | InstanceType::R5b8xlarge + | InstanceType::R5bLarge + | InstanceType::R5bMetal + | InstanceType::R5bXlarge + | InstanceType::R5d12xlarge + | InstanceType::R5d16xlarge + | InstanceType::R5d24xlarge + | InstanceType::R5d2xlarge + | InstanceType::R5d4xlarge + | InstanceType::R5d8xlarge + | InstanceType::R5dLarge + | InstanceType::R5dMetal + | InstanceType::R5dXlarge + | InstanceType::R5dn12xlarge + | InstanceType::R5dn16xlarge + | InstanceType::R5dn24xlarge + | InstanceType::R5dn2xlarge + | InstanceType::R5dn4xlarge + | InstanceType::R5dn8xlarge + | InstanceType::R5dnLarge + | InstanceType::R5dnMetal + | InstanceType::R5dnXlarge + | InstanceType::R5n12xlarge + | InstanceType::R5n16xlarge + | InstanceType::R5n24xlarge + | InstanceType::R5n2xlarge + | InstanceType::R5n4xlarge + | InstanceType::R5n8xlarge + | InstanceType::R5nLarge + | InstanceType::R5nMetal + | InstanceType::R5nXlarge + | InstanceType::R6a12xlarge + | InstanceType::R6a16xlarge + | InstanceType::R6a24xlarge + | InstanceType::R6a2xlarge + | InstanceType::R6a32xlarge + | InstanceType::R6a48xlarge + | InstanceType::R6a4xlarge + | InstanceType::R6a8xlarge + | InstanceType::R6aLarge + | InstanceType::R6aMetal + | InstanceType::R6aXlarge + | InstanceType::R6g12xlarge + | InstanceType::R6g16xlarge + | InstanceType::R6g2xlarge + | InstanceType::R6g4xlarge + | InstanceType::R6g8xlarge + | InstanceType::R6gLarge + | InstanceType::R6gMedium + | InstanceType::R6gMetal + | InstanceType::R6gXlarge + | InstanceType::R6gd12xlarge + | InstanceType::R6gd16xlarge + | InstanceType::R6gd2xlarge + | InstanceType::R6gd4xlarge + | InstanceType::R6gd8xlarge + | InstanceType::R6gdLarge + | InstanceType::R6gdMedium + | InstanceType::R6gdMetal + | InstanceType::R6gdXlarge + | InstanceType::R6i12xlarge + | InstanceType::R6i16xlarge + | InstanceType::R6i24xlarge + | InstanceType::R6i2xlarge + | InstanceType::R6i32xlarge + | InstanceType::R6i4xlarge + | InstanceType::R6i8xlarge + | InstanceType::R6iLarge + | InstanceType::R6iMetal + | InstanceType::R6iXlarge + | InstanceType::R6id12xlarge + | InstanceType::R6id16xlarge + | InstanceType::R6id24xlarge + | InstanceType::R6id2xlarge + | InstanceType::R6id32xlarge + | InstanceType::R6id4xlarge + | InstanceType::R6id8xlarge + | InstanceType::R6idLarge + | InstanceType::R6idMetal + | InstanceType::R6idXlarge + | InstanceType::R6idn12xlarge + | InstanceType::R6idn16xlarge + | InstanceType::R6idn24xlarge + | InstanceType::R6idn2xlarge + | InstanceType::R6idn32xlarge + | InstanceType::R6idn4xlarge + | InstanceType::R6idn8xlarge + | InstanceType::R6idnLarge + | InstanceType::R6idnMetal + | InstanceType::R6idnXlarge + | InstanceType::R6in12xlarge + | InstanceType::R6in16xlarge + | InstanceType::R6in24xlarge + | InstanceType::R6in2xlarge + | InstanceType::R6in32xlarge + | InstanceType::R6in4xlarge + | InstanceType::R6in8xlarge + | InstanceType::R6inLarge + | InstanceType::R6inMetal + | InstanceType::R6inXlarge + | InstanceType::R7a12xlarge + | InstanceType::R7a16xlarge + | InstanceType::R7a24xlarge + | InstanceType::R7a2xlarge + | InstanceType::R7a32xlarge + | InstanceType::R7a48xlarge + | InstanceType::R7a4xlarge + | InstanceType::R7a8xlarge + | InstanceType::R7aLarge + | InstanceType::R7aMedium + | InstanceType::R7aMetal48xl + | InstanceType::R7aXlarge + | InstanceType::R7g12xlarge + | InstanceType::R7g16xlarge + | InstanceType::R7g2xlarge + | InstanceType::R7g4xlarge + | InstanceType::R7g8xlarge + | InstanceType::R7gLarge + | InstanceType::R7gMedium + | InstanceType::R7gMetal + | InstanceType::R7gXlarge + | InstanceType::R7gd12xlarge + | InstanceType::R7gd16xlarge + | InstanceType::R7gd2xlarge + | InstanceType::R7gd4xlarge + | InstanceType::R7gd8xlarge + | InstanceType::R7gdLarge + | InstanceType::R7gdMedium + | InstanceType::R7gdXlarge + | InstanceType::R7i12xlarge + | InstanceType::R7i16xlarge + | InstanceType::R7i24xlarge + | InstanceType::R7i2xlarge + | InstanceType::R7i48xlarge + | InstanceType::R7i4xlarge + | InstanceType::R7i8xlarge + | InstanceType::R7iLarge + | InstanceType::R7iMetal24xl + | InstanceType::R7iMetal48xl + | InstanceType::R7iXlarge + | InstanceType::R7iz12xlarge + | InstanceType::R7iz16xlarge + | InstanceType::R7iz2xlarge + | InstanceType::R7iz32xlarge + | InstanceType::R7iz4xlarge + | InstanceType::R7iz8xlarge + | InstanceType::R7izLarge + | InstanceType::R7izXlarge + | InstanceType::T1Micro + | InstanceType::T22xlarge + | InstanceType::T2Large + | InstanceType::T2Medium + | InstanceType::T2Micro + | InstanceType::T2Nano + | InstanceType::T2Small + | InstanceType::T2Xlarge + | InstanceType::T32xlarge + | InstanceType::T3Large + | InstanceType::T3Medium + | InstanceType::T3Micro + | InstanceType::T3Nano + | InstanceType::T3Small + | InstanceType::T3Xlarge + | InstanceType::T3a2xlarge + | InstanceType::T3aLarge + | InstanceType::T3aMedium + | InstanceType::T3aMicro + | InstanceType::T3aNano + | InstanceType::T3aSmall + | InstanceType::T3aXlarge + | InstanceType::T4g2xlarge + | InstanceType::T4gLarge + | InstanceType::T4gMedium + | InstanceType::T4gMicro + | InstanceType::T4gNano + | InstanceType::T4gSmall + | InstanceType::T4gXlarge + | InstanceType::Trn12xlarge + | InstanceType::Trn132xlarge + | InstanceType::Trn1n32xlarge + | InstanceType::U12tb1112xlarge + | InstanceType::U12tb1Metal + | InstanceType::U18tb1112xlarge + | InstanceType::U18tb1Metal + | InstanceType::U24tb1112xlarge + | InstanceType::U24tb1Metal + | InstanceType::U3tb156xlarge + | InstanceType::U6tb1112xlarge + | InstanceType::U6tb156xlarge + | InstanceType::U6tb1Metal + | InstanceType::U9tb1112xlarge + | InstanceType::U9tb1Metal + | InstanceType::Vt124xlarge + | InstanceType::Vt13xlarge + | InstanceType::Vt16xlarge + | InstanceType::X116xlarge + | InstanceType::X132xlarge + | InstanceType::X1e16xlarge + | InstanceType::X1e2xlarge + | InstanceType::X1e32xlarge + | InstanceType::X1e4xlarge + | InstanceType::X1e8xlarge + | InstanceType::X1eXlarge + | InstanceType::X2gd12xlarge + | InstanceType::X2gd16xlarge + | InstanceType::X2gd2xlarge + | InstanceType::X2gd4xlarge + | InstanceType::X2gd8xlarge + | InstanceType::X2gdLarge + | InstanceType::X2gdMedium + | InstanceType::X2gdMetal + | InstanceType::X2gdXlarge + | InstanceType::X2idn16xlarge + | InstanceType::X2idn24xlarge + | InstanceType::X2idn32xlarge + | InstanceType::X2idnMetal + | InstanceType::X2iedn16xlarge + | InstanceType::X2iedn24xlarge + | InstanceType::X2iedn2xlarge + | InstanceType::X2iedn32xlarge + | InstanceType::X2iedn4xlarge + | InstanceType::X2iedn8xlarge + | InstanceType::X2iednMetal + | InstanceType::X2iednXlarge + | InstanceType::X2iezn12xlarge + | InstanceType::X2iezn2xlarge + | InstanceType::X2iezn4xlarge + | InstanceType::X2iezn6xlarge + | InstanceType::X2iezn8xlarge + | InstanceType::X2ieznMetal + | InstanceType::Z1d12xlarge + | InstanceType::Z1d2xlarge + | InstanceType::Z1d3xlarge + | InstanceType::Z1d6xlarge + | InstanceType::Z1dLarge + | InstanceType::Z1dMetal + | InstanceType::Z1dXlarge => Ok(instance_type), + other => Err(anyhow!("unknown InstanceType: {other:?}")), + } +} diff --git a/bin/core/src/cloud/mod.rs b/bin/core/src/cloud/mod.rs new file mode 100644 index 000000000..99d6dd2c5 --- /dev/null +++ b/bin/core/src/cloud/mod.rs @@ -0,0 +1,7 @@ +pub mod aws; + +#[derive(Debug)] +pub enum BuildCleanupData { + Server { repo_name: String }, + Aws { instance_id: String, region: String }, +} diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs new file mode 100644 index 000000000..902a7d44f --- /dev/null +++ b/bin/core/src/config.rs @@ -0,0 +1,124 @@ +use std::sync::OnceLock; + +use anyhow::Context; +use merge_config_files::parse_config_file; +use monitor_client::entities::config::core::{CoreConfig, Env}; +use serde::Deserialize; + +pub fn frontend_path() -> &'static String { + #[derive(Deserialize)] + struct FrontendEnv { + #[serde(default = "default_frontend_path")] + monitor_frontend_path: String, + } + + fn default_frontend_path() -> String { + "/frontend".to_string() + } + + static FRONTEND_PATH: OnceLock = OnceLock::new(); + FRONTEND_PATH.get_or_init(|| { + let FrontendEnv { + monitor_frontend_path, + } = envy::from_env() + .context("failed to parse FrontendEnv") + .unwrap(); + monitor_frontend_path + }) +} + +pub fn core_config() -> &'static CoreConfig { + static CORE_CONFIG: OnceLock = OnceLock::new(); + CORE_CONFIG.get_or_init(|| { + let env: Env = envy::from_env() + .context("failed to parse core Env") + .unwrap(); + let config_path = &env.monitor_config_path; + let mut config = + parse_config_file::(config_path.as_str()) + .unwrap_or_else(|e| { + panic!("failed at parsing config at {config_path} | {e:#}") + }); + + // Overrides + config.title = env.monitor_title.unwrap_or(config.title); + config.host = env.monitor_host.unwrap_or(config.host); + config.port = env.monitor_port.unwrap_or(config.port); + config.passkey = env.monitor_passkey.unwrap_or(config.passkey); + config.jwt_valid_for = + env.monitor_jwt_valid_for.unwrap_or(config.jwt_valid_for); + config.monitoring_interval = env + .monitor_monitoring_interval + .unwrap_or(config.monitoring_interval); + config.keep_stats_for_days = env + .monitor_keep_stats_for_days + .unwrap_or(config.keep_stats_for_days); + config.keep_alerts_for_days = env + .monitor_keep_alerts_for_days + .unwrap_or(config.keep_alerts_for_days); + config.github_webhook_secret = env + .monitor_github_webhook_secret + .unwrap_or(config.github_webhook_secret); + config.github_webhook_base_url = env + .monitor_github_webhook_base_url + .or(config.github_webhook_base_url); + config.docker_organizations = env + .monitor_docker_organizations + .unwrap_or(config.docker_organizations); + + config.logging.level = + env.monitor_logging_level.unwrap_or(config.logging.level); + config.logging.stdio = + env.monitor_logging_stdio.unwrap_or(config.logging.stdio); + config.logging.otlp_endpoint = env + .monitor_logging_otlp_endpoint + .or(config.logging.otlp_endpoint); + config.logging.opentelemetry_service_name = env + .monitor_logging_opentelemetry_service_name + .unwrap_or(config.logging.opentelemetry_service_name); + + config.local_auth = + env.monitor_local_auth.unwrap_or(config.local_auth); + + config.github_oauth.enabled = env + .monitor_github_oauth_enabled + .unwrap_or(config.github_oauth.enabled); + config.github_oauth.id = env + .monitor_github_oauth_id + .unwrap_or(config.github_oauth.id); + config.github_oauth.secret = env + .monitor_github_oauth_secret + .unwrap_or(config.github_oauth.secret); + + config.google_oauth.enabled = env + .monitor_google_oauth_enabled + .unwrap_or(config.google_oauth.enabled); + config.google_oauth.id = env + .monitor_google_oauth_id + .unwrap_or(config.google_oauth.id); + config.google_oauth.secret = env + .monitor_google_oauth_secret + .unwrap_or(config.google_oauth.secret); + + config.mongo.uri = env.monitor_mongo_uri.or(config.mongo.uri); + config.mongo.address = + env.monitor_mongo_address.or(config.mongo.address); + config.mongo.username = + env.monitor_mongo_username.or(config.mongo.username); + config.mongo.password = + env.monitor_mongo_password.or(config.mongo.password); + config.mongo.app_name = + env.monitor_mongo_app_name.unwrap_or(config.mongo.app_name); + config.mongo.db_name = + env.monitor_mongo_db_name.unwrap_or(config.mongo.db_name); + + config.aws.access_key_id = env + .monitor_aws_access_key_id + .unwrap_or(config.aws.access_key_id); + config.aws.secret_access_key = env + .monitor_aws_secret_access_key + .unwrap_or(config.aws.secret_access_key); + + config + }) +} diff --git a/bin/core/src/db.rs b/bin/core/src/db.rs new file mode 100644 index 000000000..9d4b4f8b8 --- /dev/null +++ b/bin/core/src/db.rs @@ -0,0 +1,117 @@ +use mongo_indexed::{create_index, create_unique_index, Indexed}; +use monitor_client::entities::{ + alert::Alert, + alerter::Alerter, + api_key::ApiKey, + build::Build, + builder::Builder, + config::core::MongoConfig, + deployment::Deployment, + permission::Permission, + procedure::Procedure, + repo::Repo, + server::{stats::SystemStatsRecord, Server}, + server_template::ServerTemplate, + tag::Tag, + update::Update, + user::User, + user_group::UserGroup, +}; +use mungos::{ + init::MongoBuilder, + mongodb::{Collection, Database}, +}; + +pub struct DbClient { + pub users: Collection, + pub user_groups: Collection, + pub permissions: Collection, + pub api_keys: Collection, + pub tags: Collection, + pub updates: Collection, + pub alerts: Collection, + pub stats: Collection, + // RESOURCES + pub servers: Collection, + pub deployments: Collection, + pub builds: Collection, + pub builders: Collection, + pub repos: Collection, + pub procedures: Collection, + pub alerters: Collection, + pub server_templates: Collection, + // + pub db: Database, +} + +impl DbClient { + pub async fn new( + MongoConfig { + uri, + address, + username, + password, + app_name, + db_name, + }: &MongoConfig, + ) -> anyhow::Result { + let mut client = MongoBuilder::default().app_name(app_name); + + match (uri, address, username, password) { + (Some(uri), _, _, _) => { + client = client.uri(uri); + } + (_, Some(address), Some(username), Some(password)) => { + client = client + .address(address) + .username(username) + .password(password); + } + (_, Some(address), _, _) => { + client = client.address(address); + } + _ => { + error!("config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?"); + std::process::exit(1) + } + } + + let client = client.build().await?; + let db = client.database(db_name); + + let client = DbClient { + users: User::collection(&db, true).await?, + user_groups: UserGroup::collection(&db, true).await?, + permissions: Permission::collection(&db, true).await?, + api_keys: ApiKey::collection(&db, true).await?, + tags: Tag::collection(&db, true).await?, + updates: Update::collection(&db, true).await?, + alerts: Alert::collection(&db, true).await?, + stats: SystemStatsRecord::collection(&db, true).await?, + servers: resource_collection(&db, "Server").await?, + deployments: resource_collection(&db, "Deployment").await?, + builds: resource_collection(&db, "Build").await?, + builders: resource_collection(&db, "Builder").await?, + repos: resource_collection(&db, "Repo").await?, + alerters: resource_collection(&db, "Alerter").await?, + procedures: resource_collection(&db, "Procedure").await?, + server_templates: resource_collection(&db, "ServerTemplate") + .await?, + db, + }; + Ok(client) + } +} + +async fn resource_collection( + db: &Database, + collection_name: &str, +) -> anyhow::Result> { + let coll = db.collection::(collection_name); + + create_unique_index(&coll, "name").await?; + + create_index(&coll, "tags").await?; + + Ok(coll) +} diff --git a/bin/core/src/helpers/action_state.rs b/bin/core/src/helpers/action_state.rs new file mode 100644 index 000000000..71d4221c5 --- /dev/null +++ b/bin/core/src/helpers/action_state.rs @@ -0,0 +1,94 @@ +use std::sync::{Arc, Mutex}; + +use anyhow::anyhow; +use monitor_client::{ + busy::Busy, + entities::{ + build::BuildActionState, deployment::DeploymentActionState, + procedure::ProcedureActionState, repo::RepoActionState, + server::ServerActionState, + }, +}; + +use super::cache::Cache; + +#[derive(Default)] +pub struct ActionStates { + pub build: Cache>>, + pub deployment: + Cache>>, + pub server: Cache>>, + pub repo: Cache>>, + pub procedure: + Cache>>, +} + +/// Need to be able to check "busy" with write lock acquired. +#[derive(Default)] +pub struct ActionState( + Mutex, +); + +impl + ActionState +{ + pub fn get(&self) -> anyhow::Result { + Ok( + *self + .0 + .lock() + .map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?, + ) + } + + pub fn busy(&self) -> anyhow::Result { + Ok( + self + .0 + .lock() + .map_err(|e| anyhow!("action state lock poisoned | {e:?}"))? + .busy(), + ) + } + + /// Will acquire lock, check busy, and if not will + /// run the provided update function on the states. + /// Returns a guard that returns the states to default (not busy) when dropped. + pub fn update( + &self, + handler: impl Fn(&mut States), + ) -> anyhow::Result> { + let mut lock = self + .0 + .lock() + .map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?; + if lock.busy() { + return Err(anyhow!("resource is busy")); + } + handler(&mut *lock); + Ok(UpdateGuard(&self.0)) + } +} + +/// When dropped will return the inner state to default. +/// The inner mutex guard must already be dropped BEFORE this is dropped, +/// which is guaranteed as the inner guard is dropped by all public methods before +/// user could drop UpdateGuard. +pub struct UpdateGuard<'a, States: Default + Send + 'static>( + &'a Mutex, +); + +impl<'a, States: Default + Send + 'static> Drop + for UpdateGuard<'a, States> +{ + fn drop(&mut self) { + let mut lock = match self.0.lock() { + Ok(lock) => lock, + Err(e) => { + error!("CRITICAL: an action state lock is poisoned | {e:?}"); + return; + } + }; + *lock = States::default(); + } +} diff --git a/bin/core/src/helpers/alert.rs b/bin/core/src/helpers/alert.rs new file mode 100644 index 000000000..7b843b038 --- /dev/null +++ b/bin/core/src/helpers/alert.rs @@ -0,0 +1,257 @@ +use anyhow::{anyhow, Context}; +use futures::future::join_all; +use monitor_client::entities::{ + alert::{Alert, AlertData}, + alerter::*, + deployment::DockerContainerState, + server::stats::SeverityLevel, +}; +use mungos::{find::find_collect, mongodb::bson::doc}; +use reqwest::StatusCode; +use slack::types::Block; + +use crate::state::db_client; + +#[instrument] +pub async fn send_alerts(alerts: &[Alert]) { + if alerts.is_empty() { + return; + } + + let alerters = find_collect( + &db_client().await.alerters, + doc! { "config.params.enabled": true }, + None, + ) + .await; + + if let Err(e) = alerters { + error!( + "ERROR sending alerts | failed to get alerters from db | {e:#}" + ); + return; + } + + let alerters = alerters.unwrap(); + + let handles = + alerts.iter().map(|alert| send_alert(&alerters, alert)); + + join_all(handles).await; +} + +#[instrument(level = "debug")] +async fn send_alert(alerters: &[Alerter], alert: &Alert) { + if alerters.is_empty() { + return; + } + + let handles = alerters.iter().map(|alerter| async { + match &alerter.config { + AlerterConfig::Slack(SlackAlerterConfig { url, enabled }) => { + if !enabled { + return Ok(()); + } + send_slack_alert(url, alert) + .await + .context("failed to send slack alert") + } + AlerterConfig::Custom(CustomAlerterConfig { url, enabled }) => { + if !enabled { + return Ok(()); + } + send_custom_alert(url, alert).await.context(format!( + "failed to send alert to custom alerter at {url}" + )) + } + } + }); + + join_all(handles) + .await + .into_iter() + .filter_map(|res| res.err()) + .for_each(|e| error!("{e:#}")); +} + +#[instrument(level = "debug")] +async fn send_custom_alert( + url: &str, + alert: &Alert, +) -> anyhow::Result<()> { + let res = reqwest::Client::new() + .post(url) + .json(alert) + .send() + .await + .context("failed at post request to alerter")?; + let status = res.status(); + if status != StatusCode::OK { + let text = res + .text() + .await + .context("failed to get response text on alerter response")?; + return Err(anyhow!( + "post to alerter failed | {status} | {text}" + )); + } + Ok(()) +} + +#[instrument(level = "debug")] +async fn send_slack_alert( + url: &str, + alert: &Alert, +) -> anyhow::Result<()> { + let level = fmt_level(alert.level); + let (text, blocks): (_, Option<_>) = match &alert.data { + AlertData::ServerUnreachable { name, region, .. } => { + let region = fmt_region(region); + match alert.level { + SeverityLevel::Ok => { + let text = + format!("{level} | *{name}*{region} is now *reachable*"); + let blocks = vec![ + Block::header(level), + Block::section(format!( + "*{name}*{region} is now *reachable*" + )), + ]; + (text, blocks.into()) + } + SeverityLevel::Critical => { + let text = + format!("{level} | *{name}*{region} is *unreachable* ❌"); + let blocks = vec![ + Block::header(level), + Block::section(format!( + "*{name}*{region} is *unreachable* ❌" + )), + ]; + (text, blocks.into()) + } + _ => unreachable!(), + } + } + AlertData::ServerCpu { + name, + region, + percentage, + .. + } => { + let region = fmt_region(region); + let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨"); + let blocks = vec![ + Block::header(format!("{level} 🚨")), + Block::section(format!( + "*{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨" + )), + ]; + (text, blocks.into()) + } + AlertData::ServerMem { + name, + region, + used_gb, + total_gb, + .. + } => { + let region = fmt_region(region); + let percentage = 100.0 * used_gb / total_gb; + let text = + format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾 🚨"); + let blocks = vec![ + Block::header(level), + Block::section(format!( + "*{name}*{region} memory usage at *{percentage:.1}%* 💾 🚨" + )), + Block::section(format!( + "using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*" + )), + ]; + (text, blocks.into()) + } + AlertData::ServerDisk { + name, + region, + path, + used_gb, + total_gb, + .. + } => { + let region = fmt_region(region); + let percentage = 100.0 * used_gb / total_gb; + let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿 🚨"); + let blocks = vec![ + Block::header(level), + Block::section(format!( + "*{name}*{region} disk usage at *{percentage:.1}%* 💿 🚨" + )), + Block::section(format!( + "mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*" + )), + ]; + (text, blocks.into()) + } + AlertData::ContainerStateChange { + name, + server_name, + from, + to, + .. + } => { + let to = fmt_docker_container_state(to); + let text = format!("📦 container *{name}* is now {to}"); + let blocks = vec![ + Block::header(text.clone()), + Block::section(format!( + "server: {server_name}\nprevious: {from}" + )), + ]; + (text, blocks.into()) + } + AlertData::AwsBuilderTerminationFailed { instance_id } => { + let text = format!( + "{level} | Failed to terminated AWS builder instance" + ); + let blocks = vec![ + Block::header(text.clone()), + Block::section(format!("instance id: {instance_id}")), + ]; + (text, blocks.into()) + } + AlertData::None {} => Default::default(), + }; + if !text.is_empty() { + let slack = slack::Client::new(url); + slack.send_message(text, blocks).await?; + } + Ok(()) +} + +fn fmt_region(region: &Option) -> String { + match region { + Some(region) => format!(" ({region})"), + None => String::new(), + } +} + +fn fmt_docker_container_state( + state: &DockerContainerState, +) -> String { + match state { + DockerContainerState::Running => String::from("Running ▶️"), + DockerContainerState::Exited => String::from("Exited 🛑"), + DockerContainerState::Restarting => String::from("Restarting 🔄"), + DockerContainerState::NotDeployed => String::from("Not Deployed"), + _ => state.to_string(), + } +} + +fn fmt_level(level: SeverityLevel) -> &'static str { + match level { + SeverityLevel::Critical => "CRITICAL 🚨", + SeverityLevel::Warning => "WARNING 🚨", + SeverityLevel::Ok => "OK ✅", + } +} diff --git a/bin/core/src/helpers/cache.rs b/bin/core/src/helpers/cache.rs new file mode 100644 index 000000000..735239a32 --- /dev/null +++ b/bin/core/src/helpers/cache.rs @@ -0,0 +1,84 @@ +use std::{collections::HashMap, hash::Hash}; + +use monitor_client::busy::Busy; +use tokio::sync::RwLock; + +#[derive(Default)] +pub struct Cache { + cache: RwLock>, +} + +impl< + K: PartialEq + Eq + Hash + std::fmt::Debug + Clone, + T: Clone + Default, + > Cache +{ + #[instrument(level = "debug", skip(self))] + pub async fn get(&self, key: &K) -> Option { + self.cache.read().await.get(key).cloned() + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_or_insert_default(&self, key: &K) -> T { + let mut lock = self.cache.write().await; + match lock.get(key).cloned() { + Some(item) => item, + None => { + let item: T = Default::default(); + lock.insert(key.clone(), item.clone()); + item + } + } + } + + #[instrument(level = "debug", skip(self))] + pub async fn get_list(&self) -> Vec { + let cache = self.cache.read().await; + cache.iter().map(|(_, e)| e.clone()).collect() + } + + #[instrument(level = "debug", skip(self))] + pub async fn insert(&self, key: Key, val: T) + where + T: std::fmt::Debug, + Key: Into + std::fmt::Debug, + { + self.cache.write().await.insert(key.into(), val); + } + + #[instrument(level = "debug", skip(self, handler))] + pub async fn update_entry( + &self, + key: Key, + handler: impl Fn(&mut T), + ) where + Key: Into + std::fmt::Debug, + { + let mut cache = self.cache.write().await; + handler(cache.entry(key.into()).or_default()); + } + + #[instrument(level = "debug", skip(self))] + pub async fn clear(&self) { + self.cache.write().await.clear(); + } + + #[instrument(level = "debug", skip(self))] + pub async fn remove(&self, key: &K) { + self.cache.write().await.remove(key); + } +} + +impl< + K: PartialEq + Eq + Hash + std::fmt::Debug + Clone, + T: Clone + Default + Busy, + > Cache +{ + #[instrument(level = "debug", skip(self))] + pub async fn busy(&self, id: &K) -> bool { + match self.get(id).await { + Some(state) => state.busy(), + None => false, + } + } +} diff --git a/bin/core/src/helpers/channel.rs b/bin/core/src/helpers/channel.rs new file mode 100644 index 000000000..467ee5d2a --- /dev/null +++ b/bin/core/src/helpers/channel.rs @@ -0,0 +1,34 @@ +use std::sync::OnceLock; + +use monitor_client::entities::update::{Update, UpdateListItem}; +use tokio::sync::{broadcast, Mutex}; + +/// A channel sending (build_id, update_id) +pub fn build_cancel_channel( +) -> &'static BroadcastChannel<(String, Update)> { + static BUILD_CANCEL_CHANNEL: OnceLock< + BroadcastChannel<(String, Update)>, + > = OnceLock::new(); + BUILD_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100)) +} + +pub fn update_channel() -> &'static BroadcastChannel { + static UPDATE_CHANNEL: OnceLock> = + OnceLock::new(); + UPDATE_CHANNEL.get_or_init(|| BroadcastChannel::new(100)) +} + +pub struct BroadcastChannel { + pub sender: Mutex>, + pub receiver: broadcast::Receiver, +} + +impl BroadcastChannel { + pub fn new(capacity: usize) -> BroadcastChannel { + let (sender, receiver) = broadcast::channel(capacity); + BroadcastChannel { + sender: sender.into(), + receiver, + } + } +} diff --git a/bin/core/src/helpers/mod.rs b/bin/core/src/helpers/mod.rs new file mode 100644 index 000000000..e424eddd0 --- /dev/null +++ b/bin/core/src/helpers/mod.rs @@ -0,0 +1,119 @@ +use std::time::Duration; + +use anyhow::{anyhow, Context}; +use monitor_client::entities::{ + permission::{Permission, PermissionLevel, UserTarget}, + server::Server, + update::ResourceTarget, + user::User, +}; +use mungos::mongodb::bson::doc; +use periphery_client::PeripheryClient; +use rand::{thread_rng, Rng}; + +use crate::{config::core_config, state::db_client}; + +pub mod action_state; +pub mod alert; +pub mod cache; +pub mod channel; +pub mod procedure; +pub mod prune; +pub mod query; +pub mod resource; +pub mod update; + +pub fn empty_or_only_spaces(word: &str) -> bool { + if word.is_empty() { + return true; + } + for char in word.chars() { + if char != ' ' { + return false; + } + } + true +} + +pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration { + Duration::from_millis(thread_rng().gen_range(min_ms..max_ms)) +} + +#[instrument] +pub async fn remove_from_recently_viewed( + resource: T, +) -> anyhow::Result<()> +where + T: Into + std::fmt::Debug, +{ + let resource: ResourceTarget = resource.into(); + let (ty, id) = resource.extract_variant_id(); + db_client() + .await + .users + .update_many( + doc! {}, + doc! { + "$pull": { + "recently_viewed": { + "type": ty.to_string(), + "id": id, + } + } + }, + None, + ) + .await + .context( + "failed to remove resource from users recently viewed", + )?; + Ok(()) +} + +// + +pub fn periphery_client( + server: &Server, +) -> anyhow::Result { + if !server.config.enabled { + return Err(anyhow!("server not enabled")); + } + + let client = PeripheryClient::new( + &server.config.address, + &core_config().passkey, + ); + + Ok(client) +} + +#[instrument] +pub async fn create_permission( + user: &User, + target: T, + level: PermissionLevel, +) where + T: Into + std::fmt::Debug, +{ + // No need to actually create permissions for admins + if user.admin { + return; + } + let target: ResourceTarget = target.into(); + if let Err(e) = db_client() + .await + .permissions + .insert_one( + Permission { + id: Default::default(), + user_target: UserTarget::User(user.id.clone()), + resource_target: target.clone(), + level, + }, + None, + ) + .await + { + error!("failed to create permission for {target:?} | {e:#}"); + }; +} diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs new file mode 100644 index 000000000..b4fcb87fd --- /dev/null +++ b/bin/core/src/helpers/procedure.rs @@ -0,0 +1,273 @@ +use std::time::{Duration, Instant}; + +use anyhow::{anyhow, Context, Ok}; +use futures::future::join_all; +use monitor_client::{ + api::execute::Execution, + entities::{ + monitor_timestamp, + procedure::{EnabledExecution, Procedure, ProcedureType}, + update::Update, + user::procedure_user, + }, +}; +use resolver_api::Resolve; +use tokio::sync::Mutex; + +use crate::state::State; + +use super::update::update_update; + +#[instrument] +pub async fn execute_procedure( + procedure: &Procedure, + update: &Mutex, +) -> anyhow::Result<()> { + let start_ts = monitor_timestamp(); + + use ProcedureType::*; + match procedure.config.procedure_type { + Sequence => { + add_line_to_update( + update, + &format!( + "executing sequence: {} ({})", + procedure.name, procedure.id + ), + ) + .await; + execute_sequence( + filter_list_by_enabled(&procedure.config.executions), + &procedure.id, + &procedure.name, + update, + ) + .await + .with_context(|| { + let time = Duration::from_millis( + (monitor_timestamp() - start_ts) as u64, + ); + format!( + "failed sequence execution after {time:?}. {} ({})", + procedure.name, procedure.id + ) + })?; + let time = Duration::from_millis( + (monitor_timestamp() - start_ts) as u64, + ); + add_line_to_update( + update, + &format!( + "finished sequence execution in {time:?}: {} ({}) ✅", + procedure.name, procedure.id + ), + ) + .await; + Ok(()) + } + Parallel => { + add_line_to_update( + update, + &format!( + "executing parallel: {} ({})", + procedure.name, procedure.id + ), + ) + .await; + execute_parallel( + filter_list_by_enabled(&procedure.config.executions), + &procedure.id, + &procedure.name, + update, + ) + .await + .with_context(|| { + let time = Duration::from_millis( + (monitor_timestamp() - start_ts) as u64, + ); + format!( + "failed parallel execution after {time:?}. {} ({})", + procedure.name, procedure.id + ) + })?; + let time = Duration::from_millis( + (monitor_timestamp() - start_ts) as u64, + ); + add_line_to_update( + update, + &format!( + "finished parallel execution in {time:?}: {} ({}) ✅", + procedure.name, procedure.id + ), + ) + .await; + Ok(()) + } + } +} + +#[instrument] +async fn execute_execution( + execution: Execution, + + // used to prevent recursive procedure + parent_id: &str, + parent_name: &str, +) -> anyhow::Result<()> { + let user = procedure_user().to_owned(); + let update = match execution { + Execution::None(_) => return Ok(()), + Execution::RunProcedure(req) => { + if req.procedure == parent_id || req.procedure == parent_name { + return Err(anyhow!("Self referential procedure detected")); + } + State + .resolve(req, user) + .await + .context("failed at RunProcedure")? + } + Execution::RunBuild(req) => State + .resolve(req, user) + .await + .context("failed at RunBuild")?, + Execution::Deploy(req) => { + State.resolve(req, user).await.context("failed at Deploy")? + } + Execution::StartContainer(req) => State + .resolve(req, user) + .await + .context("failed at StartContainer")?, + Execution::StopContainer(req) => { + State + .resolve(req, user) + .await + .context("failed at StopContainer")? + } + Execution::StopAllContainers(req) => State + .resolve(req, user) + .await + .context("failed at StopAllContainers")?, + Execution::RemoveContainer(req) => State + .resolve(req, user) + .await + .context("failed at RemoveContainer")?, + Execution::CloneRepo(req) => State + .resolve(req, user) + .await + .context("failed at CloneRepo")?, + Execution::PullRepo(req) => State + .resolve(req, user) + .await + .context("failed at PullRepo")?, + Execution::PruneDockerNetworks(req) => State + .resolve(req, user) + .await + .context("failed at PruneDockerNetworks")?, + Execution::PruneDockerImages(req) => State + .resolve(req, user) + .await + .context("failed at PruneDockerImages")?, + Execution::PruneDockerContainers(req) => State + .resolve(req, user) + .await + .context("failed at PruneDockerContainers")?, + }; + if update.success { + Ok(()) + } else { + Err(anyhow!( + "execution not successful. see update {}", + update.id + )) + } +} + +#[instrument] +async fn execute_sequence( + executions: Vec, + parent_id: &str, + parent_name: &str, + update: &Mutex, +) -> anyhow::Result<()> { + for execution in executions { + let now = Instant::now(); + add_line_to_update( + update, + &format!("executing stage: {execution:?}"), + ) + .await; + let fail_log = format!("failed on {execution:?}"); + execute_execution(execution.clone(), parent_id, parent_name) + .await + .context(fail_log)?; + add_line_to_update( + update, + &format!( + "finished stage in {:?}: {execution:?}", + now.elapsed() + ), + ) + .await; + } + Ok(()) +} + +#[instrument] +async fn execute_parallel( + executions: Vec, + parent_id: &str, + parent_name: &str, + update: &Mutex, +) -> anyhow::Result<()> { + let futures = executions.into_iter().map(|execution| async move { + let now = Instant::now(); + add_line_to_update( + update, + &format!("executing stage: {execution:?}"), + ) + .await; + let fail_log = format!("failed on {execution:?}"); + let res = + execute_execution(execution.clone(), parent_id, parent_name) + .await + .context(fail_log); + add_line_to_update( + update, + &format!( + "finished stage in {:?}: {execution:?}", + now.elapsed() + ), + ) + .await; + res + }); + join_all(futures) + .await + .into_iter() + .collect::>()?; + Ok(()) +} + +fn filter_list_by_enabled( + list: &[EnabledExecution], +) -> Vec { + list + .iter() + .filter(|item| item.enabled) + .map(|item| item.execution.clone()) + .collect() +} + +/// ASSUMES FIRST LOG IS ALREADY CREATED +#[instrument(level = "debug")] +async fn add_line_to_update(update: &Mutex, line: &str) { + let mut lock = update.lock().await; + let log = &mut lock.logs[0]; + log.stdout.push('\n'); + log.stdout.push_str(line); + let update = lock.clone(); + drop(lock); + if let Err(e) = update_update(update).await { + error!("failed to update an update during procedure | {e:#}"); + }; +} diff --git a/bin/core/src/helpers/prune.rs b/bin/core/src/helpers/prune.rs new file mode 100644 index 000000000..6781d7d53 --- /dev/null +++ b/bin/core/src/helpers/prune.rs @@ -0,0 +1,64 @@ +use async_timing_util::{ + unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS, +}; +use mungos::mongodb::bson::doc; + +use crate::{config::core_config, state::db_client}; + +pub fn spawn_prune_loop() { + tokio::spawn(async move { + loop { + wait_until_timelength(Timelength::OneDay, 5000).await; + let (stats_res, alerts_res) = + tokio::join!(prune_stats(), prune_alerts()); + if let Err(e) = stats_res { + error!("error in pruning stats | {e:#}"); + } + if let Err(e) = alerts_res { + error!("error in pruning alerts | {e:#}"); + } + } + }); +} + +async fn prune_stats() -> anyhow::Result<()> { + if core_config().keep_stats_for_days == 0 { + return Ok(()); + } + let delete_before_ts = (unix_timestamp_ms() + - core_config().keep_stats_for_days as u128 * ONE_DAY_MS) + as i64; + let res = db_client() + .await + .stats + .delete_many( + doc! { + "ts": { "$lt": delete_before_ts } + }, + None, + ) + .await?; + info!("deleted {} stats from db", res.deleted_count); + Ok(()) +} + +async fn prune_alerts() -> anyhow::Result<()> { + if core_config().keep_alerts_for_days == 0 { + return Ok(()); + } + let delete_before_ts = (unix_timestamp_ms() + - core_config().keep_alerts_for_days as u128 * ONE_DAY_MS) + as i64; + let res = db_client() + .await + .alerts + .delete_many( + doc! { + "ts": { "$lt": delete_before_ts } + }, + None, + ) + .await?; + info!("deleted {} alerts from db", res.deleted_count); + Ok(()) +} diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs new file mode 100644 index 000000000..9da82d743 --- /dev/null +++ b/bin/core/src/helpers/query.rs @@ -0,0 +1,140 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use monitor_client::entities::{ + deployment::{Deployment, DockerContainerState}, + server::{Server, ServerStatus}, + tag::Tag, + user::{admin_service_user, User}, +}; +use mungos::{ + by_id::find_one_by_id, + find::find_collect, + mongodb::bson::{doc, oid::ObjectId, Document}, +}; + +use crate::state::db_client; + +use super::resource::StateResource; + +#[instrument(level = "debug")] +pub async fn get_user(user_id: &str) -> anyhow::Result { + if let Some(user) = admin_service_user(user_id) { + return Ok(user); + } + find_one_by_id(&db_client().await.users, user_id) + .await + .context("failed to query mongo for user")? + .with_context(|| format!("no user found with id {user_id}")) +} + +#[instrument(level = "debug")] +pub async fn get_server_with_status( + server_id_or_name: &str, +) -> anyhow::Result<(Server, ServerStatus)> { + let server = Server::get_resource(server_id_or_name).await?; + if !server.config.enabled { + return Ok((server, ServerStatus::Disabled)); + } + let status = match super::periphery_client(&server)? + .request(periphery_client::api::GetHealth {}) + .await + { + Ok(_) => ServerStatus::Ok, + Err(_) => ServerStatus::NotOk, + }; + Ok((server, status)) +} + +#[instrument(level = "debug")] +pub async fn get_deployment_state( + deployment: &Deployment, +) -> anyhow::Result { + if deployment.config.server_id.is_empty() { + return Ok(DockerContainerState::NotDeployed); + } + let (server, status) = + get_server_with_status(&deployment.config.server_id).await?; + if status != ServerStatus::Ok { + return Ok(DockerContainerState::Unknown); + } + let container = super::periphery_client(&server)? + .request(periphery_client::api::container::GetContainerList {}) + .await? + .into_iter() + .find(|container| container.name == deployment.name); + + let state = match container { + Some(container) => container.state, + None => DockerContainerState::NotDeployed, + }; + + Ok(state) +} + +// TAG + +#[instrument(level = "debug")] +pub async fn get_tag(id_or_name: &str) -> anyhow::Result { + let query = match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + }; + db_client() + .await + .tags + .find_one(query, None) + .await + .context("failed to query mongo for tag")? + .with_context(|| format!("no tag found matching {id_or_name}")) +} + +#[instrument(level = "debug")] +pub async fn get_tag_check_owner( + id_or_name: &str, + user: &User, +) -> anyhow::Result { + let tag = get_tag(id_or_name).await?; + if user.admin || tag.owner == user.id { + return Ok(tag); + } + Err(anyhow!("user must be tag owner or admin")) +} + +#[instrument(level = "debug")] +pub async fn get_user_user_group_ids( + user_id: &str, +) -> anyhow::Result> { + let res = find_collect( + &db_client().await.user_groups, + doc! { + "users": user_id + }, + None, + ) + .await + .context("failed to query db for user groups")? + .into_iter() + .map(|ug| ug.id) + .collect(); + Ok(res) +} + +#[instrument(level = "debug")] +pub async fn user_target_query( + user_id: &str, +) -> anyhow::Result> { + let mut user_target_query = vec![ + doc! { "user_target.type": "User", "user_target.id": user_id }, + ]; + let user_groups = get_user_user_group_ids(user_id) + .await? + .into_iter() + .map(|ug_id| { + doc! { + "user_target.type": "UserGroup", "user_target.id": ug_id, + } + }); + user_target_query.extend(user_groups); + Ok(user_target_query) +} diff --git a/bin/core/src/helpers/resource.rs b/bin/core/src/helpers/resource.rs new file mode 100644 index 000000000..4f5a12df7 --- /dev/null +++ b/bin/core/src/helpers/resource.rs @@ -0,0 +1,737 @@ +use std::{collections::HashSet, str::FromStr}; + +use anyhow::{anyhow, Context}; +use futures::future::join_all; +use monitor_client::{ + api::write::CreateTag, + entities::{ + alerter::{ + Alerter, AlerterConfig, AlerterConfigVariant, AlerterInfo, + AlerterListItem, AlerterListItemInfo, AlerterQuerySpecifics, + }, + build::{ + Build, BuildConfig, BuildInfo, BuildListItem, + BuildListItemInfo, BuildQuerySpecifics, + }, + builder::{ + Builder, BuilderConfig, BuilderConfigVariant, BuilderListItem, + BuilderListItemInfo, BuilderQuerySpecifics, + }, + deployment::{ + Deployment, DeploymentConfig, DeploymentImage, + DeploymentListItem, DeploymentListItemInfo, + DeploymentQuerySpecifics, + }, + permission::PermissionLevel, + procedure::{ + Procedure, ProcedureConfig, ProcedureListItem, + ProcedureListItemInfo, ProcedureQuerySpecifics, + }, + repo::{ + Repo, RepoConfig, RepoInfo, RepoListItem, RepoListItemInfo, + RepoQuerySpecifics, + }, + resource::{AddFilters, Resource, ResourceQuery}, + server::{ + Server, ServerConfig, ServerListItem, ServerListItemInfo, + ServerQuerySpecifics, + }, + server_template::{ + ServerTemplate, ServerTemplateConfig, + ServerTemplateConfigVariant, ServerTemplateListItem, + ServerTemplateListItemInfo, ServerTemplateQuerySpecifics, + }, + update::{ResourceTarget, ResourceTargetVariant}, + user::User, + }, +}; +use mungos::{ + find::find_collect, + mongodb::{ + bson::{doc, oid::ObjectId, Document}, + Collection, + }, +}; +use resolver_api::Resolve; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + helpers::query::user_target_query, + state::State, + state::{db_client, deployment_status_cache, server_status_cache}, +}; + +use super::query::get_tag; + +pub trait StateResource { + type ListItem: Serialize + Send; + type Config: Send + + Sync + + Unpin + + Serialize + + DeserializeOwned + + 'static; + type Info: Send + + Sync + + Unpin + + Default + + Serialize + + DeserializeOwned + + 'static; + type QuerySpecifics: AddFilters + Default + std::fmt::Debug; + + fn name() -> &'static str; + + fn resource_target_variant() -> ResourceTargetVariant; + + async fn coll( + ) -> &'static Collection>; + + async fn to_list_item( + resource: Resource, + ) -> anyhow::Result; + + async fn get_resource( + id_or_name: &str, + ) -> anyhow::Result> { + let filter = match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + }; + Self::coll() + .await + .find_one(filter, None) + .await + .context("failed to query db for resource")? + .with_context(|| { + format!( + "did not find any {} matching {id_or_name}", + Self::name() + ) + }) + } + + async fn get_resource_check_permissions( + id_or_name: &str, + user: &User, + permission_level: PermissionLevel, + ) -> anyhow::Result> { + let resource = Self::get_resource(id_or_name).await?; + if user.admin { + return Ok(resource); + } + let permissions = + Self::get_user_permission_on_resource(&user.id, &resource.id) + .await?; + if permissions >= permission_level { + Ok(resource) + } else { + Err(anyhow!( + "user does not have required permissions on this {}", + Self::name() + )) + } + } + + async fn get_user_permission_on_resource( + user_id: &str, + resource_id: &str, + ) -> anyhow::Result { + get_user_permission_on_resource( + user_id, + Self::resource_target_variant(), + resource_id, + ) + .await + } + + async fn get_resource_ids_for_non_admin( + user_id: &str, + ) -> anyhow::Result> { + get_resource_ids_for_non_admin( + user_id, + Self::resource_target_variant(), + ) + .await + } + + async fn list_resource_list_items_for_user( + mut query: ResourceQuery, + user: &User, + ) -> anyhow::Result> { + validate_resource_query_tags(&mut query).await; + let mut filters = Document::new(); + query.add_filters(&mut filters); + Self::query_resource_list_items_for_user(filters, user).await + } + + async fn query_resource_list_items_for_user( + filters: Document, + user: &User, + ) -> anyhow::Result> { + let list = Self::query_resources_for_user(filters, user) + .await? + .into_iter() + .map(|resource| Self::to_list_item(resource)); + + let list = join_all(list) + .await + .into_iter() + .collect::>>() + .context(format!( + "failed to convert {} list item", + Self::name() + ))?; + + Ok(list) + } + + async fn list_resources_for_user( + mut query: ResourceQuery, + user: &User, + ) -> anyhow::Result>> { + validate_resource_query_tags(&mut query).await; + let mut filters = Document::new(); + query.add_filters(&mut filters); + Self::query_resources_for_user(filters, user).await + } + + async fn query_resources_for_user( + mut filters: Document, + user: &User, + ) -> anyhow::Result>> { + if !user.admin { + let ids = Self::get_resource_ids_for_non_admin(&user.id) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + filters.insert("_id", doc! { "$in": ids }); + } + find_collect(Self::coll().await, filters, None) + .await + .with_context(|| { + format!("failed to pull {}s from mongo", Self::name()) + }) + } + + async fn update_description( + id_or_name: &str, + description: &str, + user: &User, + ) -> anyhow::Result<()> { + Self::get_resource_check_permissions( + id_or_name, + user, + PermissionLevel::Write, + ) + .await?; + let filter = match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + }; + Self::coll() + .await + .update_one( + filter, + doc! { "$set": { "description": description } }, + None, + ) + .await?; + Ok(()) + } + + async fn update_tags_on_resource( + id_or_name: &str, + tags: Vec, + user: User, + ) -> anyhow::Result<()> { + let futures = tags.iter().map(|tag| async { + match get_tag(tag).await { + Ok(tag) => Ok(tag.id), + Err(_) => State + .resolve( + CreateTag { + name: tag.to_string(), + }, + user.clone(), + ) + .await + .map(|tag| tag.id), + } + }); + let tags = join_all(futures) + .await + .into_iter() + .flatten() + .collect::>(); + Self::coll() + .await + .update_one( + id_or_name_filter(id_or_name), + doc! { "$set": { "tags": tags } }, + None, + ) + .await?; + Ok(()) + } + + async fn remove_tag_from_resources( + tag_id: &str, + ) -> anyhow::Result<()> { + Self::coll() + .await + .update_many( + doc! {}, + doc! { "$pull": { "tags": tag_id } }, + None, + ) + .await + .context("failed to remove tag from resources")?; + Ok(()) + } +} + +fn id_or_name_filter(id_or_name: &str) -> Document { + match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + } +} + +impl StateResource for Server { + type ListItem = ServerListItem; + type Config = ServerConfig; + type Info = (); + type QuerySpecifics = ServerQuerySpecifics; + + fn name() -> &'static str { + "server" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Server + } + + async fn coll() -> &'static Collection { + &db_client().await.servers + } + + async fn to_list_item( + server: Server, + ) -> anyhow::Result { + let status = server_status_cache().get(&server.id).await; + Ok(ServerListItem { + name: server.name, + created_at: ObjectId::from_str(&server.id)? + .timestamp() + .timestamp_millis(), + id: server.id, + tags: server.tags, + resource_type: ResourceTargetVariant::Server, + info: ServerListItemInfo { + status: status.map(|s| s.status).unwrap_or_default(), + region: server.config.region, + send_unreachable_alerts: server + .config + .send_unreachable_alerts, + send_cpu_alerts: server.config.send_cpu_alerts, + send_mem_alerts: server.config.send_mem_alerts, + send_disk_alerts: server.config.send_disk_alerts, + }, + }) + } +} + +impl StateResource for Deployment { + type ListItem = DeploymentListItem; + type Config = DeploymentConfig; + type Info = (); + type QuerySpecifics = DeploymentQuerySpecifics; + + fn name() -> &'static str { + "deployment" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Deployment + } + + async fn coll() -> &'static Collection { + &db_client().await.deployments + } + + async fn to_list_item( + deployment: Deployment, + ) -> anyhow::Result { + let status = deployment_status_cache().get(&deployment.id).await; + let (image, build_id) = match deployment.config.image { + DeploymentImage::Build { build_id, version } => { + let build = Build::get_resource(&build_id).await?; + let version = if version.is_none() { + build.config.version.to_string() + } else { + version.to_string() + }; + (format!("{}:{version}", build.name), Some(build_id)) + } + DeploymentImage::Image { image } => (image, None), + }; + Ok(DeploymentListItem { + name: deployment.name, + created_at: ObjectId::from_str(&deployment.id)? + .timestamp() + .timestamp_millis(), + id: deployment.id, + tags: deployment.tags, + resource_type: ResourceTargetVariant::Deployment, + info: DeploymentListItemInfo { + state: status + .as_ref() + .map(|s| s.curr.state) + .unwrap_or_default(), + status: status.as_ref().and_then(|s| { + s.curr.container.as_ref().and_then(|c| c.status.to_owned()) + }), + image, + server_id: deployment.config.server_id, + build_id, + }, + }) + } +} + +impl StateResource for Build { + type ListItem = BuildListItem; + type Config = BuildConfig; + type Info = BuildInfo; + type QuerySpecifics = BuildQuerySpecifics; + + fn name() -> &'static str { + "build" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Build + } + + async fn coll() -> &'static Collection { + &db_client().await.builds + } + + async fn to_list_item( + build: Build, + ) -> anyhow::Result { + Ok(BuildListItem { + name: build.name, + created_at: ObjectId::from_str(&build.id)? + .timestamp() + .timestamp_millis(), + id: build.id, + tags: build.tags, + resource_type: ResourceTargetVariant::Build, + info: BuildListItemInfo { + last_built_at: build.info.last_built_at, + version: build.config.version, + repo: build.config.repo, + branch: build.config.branch, + }, + }) + } +} + +impl StateResource for Repo { + type ListItem = RepoListItem; + type Config = RepoConfig; + type Info = RepoInfo; + type QuerySpecifics = RepoQuerySpecifics; + + fn name() -> &'static str { + "repo" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Repo + } + + async fn coll() -> &'static Collection { + &db_client().await.repos + } + + async fn to_list_item(repo: Repo) -> anyhow::Result { + Ok(RepoListItem { + name: repo.name, + created_at: ObjectId::from_str(&repo.id)? + .timestamp() + .timestamp_millis(), + id: repo.id, + tags: repo.tags, + resource_type: ResourceTargetVariant::Repo, + info: RepoListItemInfo { + last_pulled_at: repo.info.last_pulled_at, + repo: repo.config.repo, + branch: repo.config.branch, + }, + }) + } +} + +impl StateResource for Builder { + type ListItem = BuilderListItem; + type Config = BuilderConfig; + type Info = (); + type QuerySpecifics = BuilderQuerySpecifics; + + fn name() -> &'static str { + "builder" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Builder + } + + async fn coll() -> &'static Collection { + &db_client().await.builders + } + + async fn to_list_item( + builder: Builder, + ) -> anyhow::Result { + let (builder_type, instance_type) = match builder.config { + BuilderConfig::Server(config) => ( + BuilderConfigVariant::Server.to_string(), + Some(config.server_id), + ), + BuilderConfig::Aws(config) => ( + BuilderConfigVariant::Aws.to_string(), + Some(config.instance_type), + ), + }; + + Ok(BuilderListItem { + name: builder.name, + created_at: ObjectId::from_str(&builder.id)? + .timestamp() + .timestamp_millis(), + id: builder.id, + tags: builder.tags, + resource_type: ResourceTargetVariant::Builder, + info: BuilderListItemInfo { + builder_type, + instance_type, + }, + }) + } +} + +impl StateResource for Alerter { + type ListItem = AlerterListItem; + type Config = AlerterConfig; + type Info = AlerterInfo; + type QuerySpecifics = AlerterQuerySpecifics; + + fn name() -> &'static str { + "alerter" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Alerter + } + + async fn coll() -> &'static Collection { + &db_client().await.alerters + } + + async fn to_list_item( + alerter: Alerter, + ) -> anyhow::Result { + let (alerter_type, enabled) = match alerter.config { + AlerterConfig::Custom(config) => { + (AlerterConfigVariant::Custom.to_string(), config.enabled) + } + AlerterConfig::Slack(config) => { + (AlerterConfigVariant::Slack.to_string(), config.enabled) + } + }; + Ok(AlerterListItem { + name: alerter.name, + created_at: ObjectId::from_str(&alerter.id)? + .timestamp() + .timestamp_millis(), + id: alerter.id, + tags: alerter.tags, + resource_type: ResourceTargetVariant::Alerter, + info: AlerterListItemInfo { + alerter_type: alerter_type.to_string(), + is_default: alerter.info.is_default, + enabled, + }, + }) + } +} + +impl StateResource for Procedure { + type ListItem = ProcedureListItem; + type Config = ProcedureConfig; + type Info = (); + type QuerySpecifics = ProcedureQuerySpecifics; + + fn name() -> &'static str { + "procedure" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Procedure + } + + async fn coll() -> &'static Collection { + &db_client().await.procedures + } + + async fn to_list_item( + procedure: Procedure, + ) -> anyhow::Result { + Ok(ProcedureListItem { + name: procedure.name, + created_at: ObjectId::from_str(&procedure.id)? + .timestamp() + .timestamp_millis(), + id: procedure.id, + tags: procedure.tags, + resource_type: ResourceTargetVariant::Procedure, + info: ProcedureListItemInfo { + procedure_type: procedure.config.procedure_type, + }, + }) + } +} + +impl StateResource for ServerTemplate { + type ListItem = ServerTemplateListItem; + type Config = ServerTemplateConfig; + type Info = (); + type QuerySpecifics = ServerTemplateQuerySpecifics; + + fn name() -> &'static str { + "server_template" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Alerter + } + + async fn coll() -> &'static Collection { + &db_client().await.server_templates + } + + async fn to_list_item( + server_template: ServerTemplate, + ) -> anyhow::Result { + let (template_type, instance_type) = match server_template.config + { + ServerTemplateConfig::Aws(config) => ( + ServerTemplateConfigVariant::Aws.to_string(), + Some(config.instance_type), + ), + }; + Ok(ServerTemplateListItem { + name: server_template.name, + created_at: ObjectId::from_str(&server_template.id)? + .timestamp() + .timestamp_millis(), + id: server_template.id, + tags: server_template.tags, + resource_type: ResourceTargetVariant::ServerTemplate, + info: ServerTemplateListItemInfo { + provider: template_type.to_string(), + instance_type, + }, + }) + } +} + +#[instrument(level = "debug")] +pub async fn get_user_permission_on_resource( + user_id: &str, + resource_variant: ResourceTargetVariant, + resource_id: &str, +) -> anyhow::Result { + let permission = find_collect( + &db_client().await.permissions, + doc! { + "$or": user_target_query(user_id).await?, + "resource_target.type": resource_variant.as_ref(), + "resource_target.id": resource_id + }, + None, + ) + .await + .context("failed to query db for permissions")? + .into_iter() + // get the max permission user has between personal / any user groups + .fold(PermissionLevel::None, |level, permission| { + if permission.level > level { + permission.level + } else { + level + } + }); + Ok(permission) +} + +#[instrument] +pub async fn delete_all_permissions_on_resource(target: T) +where + T: Into + std::fmt::Debug, +{ + let target: ResourceTarget = target.into(); + let (variant, id) = target.extract_variant_id(); + if let Err(e) = db_client() + .await + .permissions + .delete_many( + doc! { + "resource_target.type": variant.as_ref(), + "resource_target.id": &id + }, + None, + ) + .await + { + warn!("failed to delete_many permissions matching target {target:?} | {e:#}"); + } +} + +#[instrument(level = "debug")] +pub async fn get_resource_ids_for_non_admin( + user_id: &str, + resource_type: ResourceTargetVariant, +) -> anyhow::Result> { + let permissions = find_collect( + &db_client().await.permissions, + doc! { + "$or": user_target_query(user_id).await?, + "resource_target.type": resource_type.as_ref(), + "level": { "$in": ["Read", "Execute", "Write"] } + }, + None, + ) + .await + .context("failed to query permissions on db")? + .into_iter() + .map(|p| p.resource_target.extract_variant_id().1.to_string()) + // collect into hashset first to remove any duplicates + .collect::>(); + Ok(permissions.into_iter().collect()) +} + +#[instrument(level = "debug")] +pub async fn validate_resource_query_tags< + T: Default + std::fmt::Debug, +>( + query: &mut ResourceQuery, +) { + let futures = query.tags.iter().map(|tag| get_tag(tag)); + let res = join_all(futures).await; + query.tags = res.into_iter().flatten().map(|tag| tag.id).collect(); +} diff --git a/bin/core/src/helpers/update.rs b/bin/core/src/helpers/update.rs new file mode 100644 index 000000000..95afd56a1 --- /dev/null +++ b/bin/core/src/helpers/update.rs @@ -0,0 +1,95 @@ +use anyhow::Context; +use monitor_client::entities::{ + monitor_timestamp, + update::{ResourceTarget, Update, UpdateListItem}, + user::User, + Operation, +}; +use mungos::{ + by_id::{find_one_by_id, update_one_by_id}, + mongodb::bson::to_document, +}; + +use crate::state::db_client; + +use super::channel::update_channel; + +pub fn make_update( + target: impl Into, + operation: Operation, + user: &User, +) -> Update { + Update { + start_ts: monitor_timestamp(), + target: target.into(), + operation, + operator: user.id.clone(), + success: true, + ..Default::default() + } +} + +#[instrument(level = "debug")] +async fn update_list_item( + update: Update, +) -> anyhow::Result { + let username = if User::is_service_user(&update.operator) { + update.operator.clone() + } else { + find_one_by_id(&db_client().await.users, &update.operator) + .await + .context("failed to query mongo for user")? + .with_context(|| { + format!("no user found with id {}", update.operator) + })? + .username + }; + let update = UpdateListItem { + id: update.id, + operation: update.operation, + start_ts: update.start_ts, + success: update.success, + operator: update.operator, + target: update.target, + status: update.status, + version: update.version, + username, + }; + Ok(update) +} + +#[instrument(level = "debug")] +async fn send_update(update: UpdateListItem) -> anyhow::Result<()> { + update_channel().sender.lock().await.send(update)?; + Ok(()) +} + +#[instrument(level = "debug")] +pub async fn add_update( + mut update: Update, +) -> anyhow::Result { + update.id = db_client() + .await + .updates + .insert_one(&update, None) + .await + .context("failed to insert update into db")? + .inserted_id + .as_object_id() + .context("inserted_id is not object id")? + .to_string(); + let id = update.id.clone(); + let update = update_list_item(update).await?; + let _ = send_update(update).await; + Ok(id) +} + +#[instrument(level = "debug")] +pub async fn update_update(update: Update) -> anyhow::Result<()> { + update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None) + .await + .context("failed to update the update on db. the update build process was deleted")?; + let update = update_list_item(update).await?; + let _ = send_update(update).await; + Ok(()) +} diff --git a/bin/core/src/listener/github.rs b/bin/core/src/listener/github.rs new file mode 100644 index 000000000..f2f48b92c --- /dev/null +++ b/bin/core/src/listener/github.rs @@ -0,0 +1,238 @@ +use anyhow::{anyhow, Context}; +use axum::{extract::Path, http::HeaderMap, routing::post, Router}; +use hex::ToHex; +use hmac::{Hmac, Mac}; +use monitor_client::{ + api::execute, + entities::{build::Build, repo::Repo, user::github_user}, +}; +use resolver_api::Resolve; +use serde::Deserialize; +use sha2::Sha256; +use tracing::Instrument; + +use crate::{ + config::core_config, + helpers::{random_duration, resource::StateResource}, + state::State, +}; + +type HmacSha256 = Hmac; + +#[derive(Deserialize)] +struct Id { + id: String, +} + +#[derive(Deserialize)] +struct IdBranch { + id: String, + branch: String, +} + +pub fn router() -> Router { + Router::new() + .route( + "/build/:id", + post( + |Path(Id { id }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("build_webhook", id); + async { + let res = handle_build_webhook(id.clone(), headers, body).await; + if let Err(e) = res { + warn!("failed to run build webook for build {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ), + ) + .route( + "/repo/:id/clone", + post( + |Path(Id { id }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("repo_clone_webhook", id); + async { + let res = handle_repo_clone_webhook(id.clone(), headers, body).await; + if let Err(e) = res { + warn!("failed to run repo clone webook for repo {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ) + ) + .route( + "/repo/:id/pull", + post( + |Path(Id { id }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("repo_pull_webhook", id); + async { + let res = handle_repo_pull_webhook(id.clone(), headers, body).await; + if let Err(e) = res { + warn!("failed to run repo pull webook for repo {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ) + ) + .route( + "/procedure/:id/:branch", + post( + |Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("procedure_webhook", id, branch); + async { + let res = handle_procedure_webhook( + id.clone(), + branch, + headers, + body + ).await; + if let Err(e) = res { + warn!("failed to run procedure webook for procedure {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ) + ) +} + +async fn handle_build_webhook( + build_id: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + let build = Build::get_resource(&build_id).await?; + if request_branch != build.config.branch { + return Err(anyhow!("request branch does not match expected")); + } + State + .resolve( + execute::RunBuild { build: build_id }, + github_user().to_owned(), + ) + .await?; + Ok(()) +} + +async fn handle_repo_clone_webhook( + repo_id: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + let repo = Repo::get_resource(&repo_id).await?; + if request_branch != repo.config.branch { + return Err(anyhow!("request branch does not match expected")); + } + State + .resolve( + execute::CloneRepo { repo: repo_id }, + github_user().to_owned(), + ) + .await?; + Ok(()) +} + +async fn handle_repo_pull_webhook( + repo_id: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + let repo = Repo::get_resource(&repo_id).await?; + if request_branch != repo.config.branch { + return Err(anyhow!("request branch does not match expected")); + } + State + .resolve( + execute::PullRepo { repo: repo_id }, + github_user().to_owned(), + ) + .await?; + Ok(()) +} + +async fn handle_procedure_webhook( + procedure_id: String, + target_branch: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + if request_branch != target_branch { + return Err(anyhow!("request branch does not match expected")); + } + State + .resolve( + execute::RunProcedure { + procedure: procedure_id, + }, + github_user().to_owned(), + ) + .await?; + Ok(()) +} + +#[instrument(skip_all)] +async fn verify_gh_signature( + headers: HeaderMap, + body: &str, +) -> anyhow::Result<()> { + // wait random amount of time + tokio::time::sleep(random_duration(0, 500)).await; + + let signature = headers.get("x-hub-signature-256"); + if signature.is_none() { + return Err(anyhow!("no signature in headers")); + } + let signature = signature.unwrap().to_str(); + if signature.is_err() { + return Err(anyhow!("failed to unwrap signature")); + } + let signature = signature.unwrap().replace("sha256=", ""); + let mut mac = HmacSha256::new_from_slice( + core_config().github_webhook_secret.as_bytes(), + ) + .expect("github webhook | failed to create hmac sha256"); + mac.update(body.as_bytes()); + let expected = mac.finalize().into_bytes().encode_hex::(); + if signature == expected { + Ok(()) + } else { + Err(anyhow!("signature does not equal expected")) + } +} + +#[derive(Deserialize)] +struct GithubWebhookBody { + #[serde(rename = "ref")] + branch: String, +} + +fn extract_branch(body: &str) -> anyhow::Result { + let branch = serde_json::from_str::(body) + .context("failed to parse github request body")? + .branch + .replace("refs/heads/", ""); + Ok(branch) +} diff --git a/bin/core/src/listener/mod.rs b/bin/core/src/listener/mod.rs new file mode 100644 index 000000000..f6603429b --- /dev/null +++ b/bin/core/src/listener/mod.rs @@ -0,0 +1,7 @@ +use axum::Router; + +mod github; + +pub fn router() -> Router { + Router::new().nest("/github", github::router()) +} diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs new file mode 100644 index 000000000..b244b0fe3 --- /dev/null +++ b/bin/core/src/main.rs @@ -0,0 +1,91 @@ +#[macro_use] +extern crate tracing; + +use std::{net::SocketAddr, str::FromStr}; + +use anyhow::Context; +use axum::Router; +use termination_signal::tokio::immediate_term_handle; +use tower_http::{ + cors::{Any, CorsLayer}, + services::{ServeDir, ServeFile}, +}; + +use crate::config::{core_config, frontend_path}; + +mod api; +mod auth; +mod cloud; +mod config; +mod db; +mod helpers; +mod listener; +mod monitor; +mod state; +mod ws; + +async fn app() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + let config = core_config(); + logger::init(&config.logging)?; + info!("monitor core version: v{}", env!("CARGO_PKG_VERSION")); + info!("config: {:?}", config.sanitized()); + + // Spawn monitoring loops + monitor::spawn_monitor_loop(); + helpers::prune::spawn_prune_loop(); + + // Setup static frontend services + let frontend_path = frontend_path(); + let frontend_index = + ServeFile::new(format!("{frontend_path}/index.html")); + let serve_dir = ServeDir::new(frontend_path) + .not_found_service(frontend_index.clone()); + + let app = Router::new() + .nest("/auth", api::auth::router()) + .nest("/read", api::read::router()) + .nest("/write", api::write::router()) + .nest("/execute", api::execute::router()) + .nest("/listener", listener::router()) + .nest("/ws", ws::router()) + .nest_service("/", serve_dir) + .fallback_service(frontend_index) + .layer(cors()?); + + let socket_addr = + SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port)) + .context("failed to parse socket addr")?; + + let listener = tokio::net::TcpListener::bind(&socket_addr) + .await + .context("failed to bind to tcp listener")?; + + info!("monitor core listening on {socket_addr}"); + + axum::serve(listener, app).await.context("server crashed")?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let term_signal = immediate_term_handle()?; + + let app = tokio::spawn(app()); + + tokio::select! { + res = app => return res?, + _ = term_signal => {}, + } + + Ok(()) +} + +fn cors() -> anyhow::Result { + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + Ok(cors) +} diff --git a/bin/core/src/monitor/alert/deployment.rs b/bin/core/src/monitor/alert/deployment.rs new file mode 100644 index 000000000..e0e96f226 --- /dev/null +++ b/bin/core/src/monitor/alert/deployment.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use monitor_client::entities::{ + alert::{Alert, AlertData, AlertDataVariant}, + deployment::Deployment, + server::stats::SeverityLevel, + update::ResourceTarget, +}; + +use crate::{ + helpers::{alert::send_alerts, resource::StateResource}, + monitor::deployment_status_cache, + state::db_client, +}; + +#[instrument(level = "debug")] +pub async fn alert_deployments( + ts: i64, + server_names: HashMap, +) { + let mut alerts = Vec::::new(); + for v in deployment_status_cache().get_list().await { + if v.prev.is_none() { + continue; + } + let prev = v.prev.as_ref().unwrap().to_owned(); + if v.curr.state != prev { + // send alert + let d = Deployment::get_resource(&v.curr.id).await; + if let Err(e) = d { + error!("failed to get deployment from db | {e:#?}"); + continue; + } + let d = d.unwrap(); + let target: ResourceTarget = (&d).into(); + let data = AlertData::ContainerStateChange { + id: v.curr.id.clone(), + name: d.name, + server_name: server_names + .get(&d.config.server_id) + .cloned() + .unwrap_or(String::from("unknown")), + server_id: d.config.server_id, + from: prev, + to: v.curr.state, + }; + let alert = Alert { + id: Default::default(), + level: SeverityLevel::Warning, + variant: AlertDataVariant::ContainerStateChange, + resolved: true, + resolved_ts: ts.into(), + target, + data, + ts, + }; + if d.config.send_alerts { + alerts.push(alert); + } + } + } + if alerts.is_empty() { + return; + } + send_alerts(&alerts).await; + let res = db_client().await.alerts.insert_many(alerts, None).await; + if let Err(e) = res { + error!("failed to record deployment status alerts to db | {e:#}"); + } +} diff --git a/bin/core/src/monitor/alert/mod.rs b/bin/core/src/monitor/alert/mod.rs new file mode 100644 index 000000000..a59f1ddaa --- /dev/null +++ b/bin/core/src/monitor/alert/mod.rs @@ -0,0 +1,59 @@ +mod deployment; +mod server; + +use std::collections::HashMap; + +use anyhow::Context; +use monitor_client::entities::{ + resource::ResourceQuery, + server::{Server, ServerListItem}, + user::User, +}; + +use crate::helpers::resource::StateResource; + +// called after cache update +#[instrument(level = "debug")] +pub async fn check_alerts(ts: i64) { + let servers = get_all_servers_map().await; + + if let Err(e) = servers { + error!("{e:#?}"); + return; + } + + let (servers, server_names) = servers.unwrap(); + + tokio::join!( + server::alert_servers(ts, servers), + deployment::alert_deployments(ts, server_names) + ); +} + +#[instrument(level = "debug")] +async fn get_all_servers_map() -> anyhow::Result<( + HashMap, + HashMap, +)> { + let servers = Server::list_resource_list_items_for_user( + ResourceQuery::default(), + &User { + admin: true, + ..Default::default() + }, + ) + .await + .context("failed to get servers from db (in alert_servers)")?; + + let servers = servers + .into_iter() + .map(|server| (server.id.clone(), server)) + .collect::>(); + + let server_names = servers + .iter() + .map(|(id, server)| (id.clone(), server.name.clone())) + .collect::>(); + + Ok((servers, server_names)) +} diff --git a/bin/core/src/monitor/alert/server.rs b/bin/core/src/monitor/alert/server.rs new file mode 100644 index 000000000..2cc4bcaa7 --- /dev/null +++ b/bin/core/src/monitor/alert/server.rs @@ -0,0 +1,501 @@ +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +use anyhow::Context; +use mongo_indexed::Indexed; +use monitor_client::entities::{ + alert::{Alert, AlertData, AlertDataVariant}, + monitor_timestamp, optional_string, + server::{stats::SeverityLevel, ServerListItem, ServerStatus}, + update::ResourceTarget, +}; +use mungos::{ + bulk_update::{self, BulkUpdate}, + find::find_collect, + mongodb::bson::{doc, oid::ObjectId, to_bson}, +}; + +use crate::{ + helpers::alert::send_alerts, + state::{db_client, server_status_cache}, +}; + +type SendAlerts = bool; +type OpenAlertMap = + HashMap>; +type OpenDiskAlertMap = OpenAlertMap; + +#[instrument(level = "debug")] +pub async fn alert_servers( + ts: i64, + mut servers: HashMap, +) { + let server_statuses = server_status_cache().get_list().await; + + let (alerts, disk_alerts) = match get_open_alerts().await { + Ok(alerts) => alerts, + Err(e) => { + error!("{e:#}"); + return; + } + }; + + let mut alerts_to_open = Vec::<(Alert, SendAlerts)>::new(); + let mut alerts_to_update = Vec::<(Alert, SendAlerts)>::new(); + let mut alert_ids_to_close = Vec::<(String, SendAlerts)>::new(); + + for server_status in server_statuses { + let Some(server) = servers.remove(&server_status.id) else { + continue; + }; + let server_alerts = + alerts.get(&ResourceTarget::Server(server_status.id.clone())); + + // =================== + // SERVER HEALTH + // =================== + let health_alert = server_alerts.as_ref().and_then(|alerts| { + alerts.get(&AlertDataVariant::ServerUnreachable) + }); + match (server_status.status, health_alert) { + (ServerStatus::NotOk, None) => { + // open unreachable alert + let alert = Alert { + id: Default::default(), + ts, + resolved: false, + resolved_ts: None, + level: SeverityLevel::Critical, + target: ResourceTarget::Server(server_status.id.clone()), + variant: AlertDataVariant::ServerUnreachable, + data: AlertData::ServerUnreachable { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + err: server_status.err.clone(), + }, + }; + alerts_to_open + .push((alert, server.info.send_unreachable_alerts)) + } + (ServerStatus::NotOk, Some(alert)) => { + // update alert err + let mut alert = alert.clone(); + let (id, name, region) = match alert.data { + AlertData::ServerUnreachable { + id, name, region, .. + } => (id, name, region), + data => { + error!("got incorrect alert data in ServerStatus handler. got {data:?}"); + continue; + } + }; + alert.data = AlertData::ServerUnreachable { + id, + name, + region, + err: server_status.err.clone(), + }; + + // Never send this alert, severity is always 'Critical' + alerts_to_update.push((alert, false)); + } + + // Close an open alert + ( + ServerStatus::Ok | ServerStatus::Disabled, + Some(health_alert), + ) => alert_ids_to_close.push(( + health_alert.id.clone(), + server.info.send_unreachable_alerts, + )), + _ => {} + } + + let Some(health) = &server_status.health else { + continue; + }; + + // =================== + // SERVER CPU + // =================== + let cpu_alert = server_alerts + .as_ref() + .and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu)) + .cloned(); + match (health.cpu, cpu_alert) { + (SeverityLevel::Warning | SeverityLevel::Critical, None) => { + // open alert + let alert = Alert { + id: Default::default(), + ts, + resolved: false, + resolved_ts: None, + level: health.cpu, + target: ResourceTarget::Server(server_status.id.clone()), + variant: AlertDataVariant::ServerCpu, + data: AlertData::ServerCpu { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + percentage: server_status + .stats + .as_ref() + .map(|s| s.cpu_perc as f64) + .unwrap_or(0.0), + }, + }; + alerts_to_open.push((alert, server.info.send_cpu_alerts)); + } + ( + SeverityLevel::Warning | SeverityLevel::Critical, + Some(mut alert), + ) => { + // modify alert level + if alert.level != health.cpu { + alert.level = health.cpu; + alert.data = AlertData::ServerCpu { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + percentage: server_status + .stats + .as_ref() + .map(|s| s.cpu_perc as f64) + .unwrap_or(0.0), + }; + alerts_to_update.push((alert, server.info.send_cpu_alerts)); + } + } + (SeverityLevel::Ok, Some(alert)) => alert_ids_to_close + .push((alert.id.clone(), server.info.send_cpu_alerts)), + _ => {} + } + + // =================== + // SERVER MEM + // =================== + let mem_alert = server_alerts + .as_ref() + .and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem)) + .cloned(); + match (health.mem, mem_alert) { + (SeverityLevel::Warning | SeverityLevel::Critical, None) => { + // open alert + let alert = Alert { + id: Default::default(), + ts, + resolved: false, + resolved_ts: None, + level: health.cpu, + target: ResourceTarget::Server(server_status.id.clone()), + variant: AlertDataVariant::ServerMem, + data: AlertData::ServerMem { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + total_gb: server_status + .stats + .as_ref() + .map(|s| s.mem_total_gb) + .unwrap_or(0.0), + used_gb: server_status + .stats + .as_ref() + .map(|s| s.mem_used_gb) + .unwrap_or(0.0), + }, + }; + alerts_to_open.push((alert, server.info.send_mem_alerts)); + } + ( + SeverityLevel::Warning | SeverityLevel::Critical, + Some(mut alert), + ) => { + if alert.level != health.mem { + alert.level = health.mem; + alert.data = AlertData::ServerMem { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + total_gb: server_status + .stats + .as_ref() + .map(|s| s.mem_total_gb) + .unwrap_or(0.0), + used_gb: server_status + .stats + .as_ref() + .map(|s| s.mem_used_gb) + .unwrap_or(0.0), + }; + alerts_to_update.push((alert, server.info.send_mem_alerts)); + } + } + (SeverityLevel::Ok, Some(alert)) => alert_ids_to_close + .push((alert.id.clone(), server.info.send_mem_alerts)), + _ => {} + } + + // =================== + // SERVER DISK + // =================== + + let server_disk_alerts = disk_alerts + .get(&ResourceTarget::Server(server_status.id.clone())); + + for (path, health) in &health.disks { + let disk_alert = server_disk_alerts + .as_ref() + .and_then(|alerts| alerts.get(path)) + .cloned(); + match (*health, disk_alert) { + (SeverityLevel::Warning | SeverityLevel::Critical, None) => { + let disk = server_status.stats.as_ref().and_then(|stats| { + stats.disks.iter().find(|disk| disk.mount == *path) + }); + let alert = Alert { + id: Default::default(), + ts, + resolved: false, + resolved_ts: None, + level: *health, + target: ResourceTarget::Server(server_status.id.clone()), + variant: AlertDataVariant::ServerDisk, + data: AlertData::ServerDisk { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + path: path.to_owned(), + total_gb: disk.map(|d| d.total_gb).unwrap_or_default(), + used_gb: disk.map(|d| d.used_gb).unwrap_or_default(), + }, + }; + alerts_to_open.push((alert, server.info.send_disk_alerts)); + } + ( + SeverityLevel::Warning | SeverityLevel::Critical, + Some(mut alert), + ) => { + if *health != alert.level { + let disk = + server_status.stats.as_ref().and_then(|stats| { + stats.disks.iter().find(|disk| disk.mount == *path) + }); + alert.level = *health; + alert.data = AlertData::ServerDisk { + id: server_status.id.clone(), + name: server.name.clone(), + region: optional_string(&server.info.region), + path: path.to_owned(), + total_gb: disk.map(|d| d.total_gb).unwrap_or_default(), + used_gb: disk.map(|d| d.used_gb).unwrap_or_default(), + }; + alerts_to_update + .push((alert, server.info.send_disk_alerts)); + } + } + (SeverityLevel::Ok, Some(alert)) => alert_ids_to_close + .push((alert.id.clone(), server.info.send_disk_alerts)), + _ => {} + } + } + } + + tokio::join!( + open_alerts(&alerts_to_open), + update_alerts(&alerts_to_update), + resolve_alerts(&alert_ids_to_close), + ); +} + +#[instrument(level = "debug")] +async fn open_alerts(alerts: &[(Alert, SendAlerts)]) { + if alerts.is_empty() { + return; + } + + let db = db_client().await; + + let open = || async { + let ids = db + .alerts + .insert_many(alerts.iter().map(|(alert, _)| alert), None) + .await? + .inserted_ids + .into_iter() + .filter_map(|(index, id)| { + alerts.get(index)?.1.then(|| id.as_object_id()) + }) + .flatten() + .collect::>(); + anyhow::Ok(ids) + }; + + let ids_to_send = match open().await { + Ok(ids) => ids, + Err(e) => { + error!("failed to open alerts on db | {e:?}"); + return; + } + }; + + let alerts = match find_collect( + &db.alerts, + doc! { "_id": { "$in": ids_to_send } }, + None, + ) + .await + { + Ok(alerts) => alerts, + Err(e) => { + error!("failed to pull created alerts from mongo | {e:?}"); + return; + } + }; + + send_alerts(&alerts).await +} + +#[instrument(level = "debug")] +async fn update_alerts(alerts: &[(Alert, SendAlerts)]) { + if alerts.is_empty() { + return; + } + + let open = || async { + let updates = alerts.iter().map(|(alert, _)| { + let update = BulkUpdate { + query: doc! { "_id": ObjectId::from_str(&alert.id).context("failed to convert alert id to ObjectId")? }, + update: doc! { "$set": to_bson(alert).context("failed to convert alert to bson")? } + }; + anyhow::Ok(update) + }) + .filter_map(|update| match update { + Ok(update) => Some(update), + Err(e) => { + warn!("failed to generate bulk update for alert | {e:#}"); + None + } + }).collect::>(); + + bulk_update::bulk_update( + &db_client().await.db, + Alert::default_collection_name(), + &updates, + false, + ) + .await + .context("failed to bulk update alerts")?; + + anyhow::Ok(()) + }; + + let alerts = alerts + .iter() + .filter(|(_, send)| *send) + .map(|(alert, _)| alert) + .cloned() + .collect::>(); + + let (res, _) = tokio::join!(open(), send_alerts(&alerts)); + + if let Err(e) = res { + error!("failed to create alerts on db | {e:#}"); + } +} + +#[instrument(level = "debug")] +async fn resolve_alerts(alert_ids: &[(String, SendAlerts)]) { + if alert_ids.is_empty() { + return; + } + + let send_alerts_map = + alert_ids.iter().cloned().collect::>(); + + let close = || async { + let alert_ids = alert_ids + .iter() + .map(|(id, _)| { + ObjectId::from_str(id) + .context("failed to convert alert id to ObjectId") + }) + .collect::>>()?; + db_client() + .await + .alerts + .update_many( + doc! { "_id": { "$in": &alert_ids } }, + doc! { + "$set": { + "resolved": true, + "resolved_ts": monitor_timestamp() + } + }, + None, + ) + .await + .context("failed to resolve alerts on db")?; + let mut closed = find_collect( + &db_client().await.alerts, + doc! { "_id": { "$in": &alert_ids } }, + None, + ) + .await + .context("failed to get closed alerts from db")?; + + for closed in &mut closed { + closed.level = SeverityLevel::Ok; + } + + let closed = closed + .into_iter() + .filter(|closed| { + if let ResourceTarget::Server(id) = &closed.target { + send_alerts_map.get(id).cloned().unwrap_or(true) + } else { + error!("got resource target other than server in resolve_server_alerts"); + true + } + }) + .collect::>(); + + send_alerts(&closed).await; + + anyhow::Ok(()) + }; + + if let Err(e) = close().await { + error!("failed to resolve alerts | {e:#?}"); + } +} + +#[instrument(level = "debug")] +async fn get_open_alerts( +) -> anyhow::Result<(OpenAlertMap, OpenDiskAlertMap)> { + let alerts = find_collect( + &db_client().await.alerts, + doc! { "resolved": false }, + None, + ) + .await + .context("failed to get open alerts from db")?; + + let mut map = OpenAlertMap::new(); + let mut disk_map = OpenDiskAlertMap::new(); + + for alert in alerts { + match &alert.data { + AlertData::ServerDisk { path, .. } => { + let inner = disk_map.entry(alert.target.clone()).or_default(); + inner.insert(path.to_owned(), alert); + } + _ => { + let inner = map.entry(alert.target.clone()).or_default(); + inner.insert(alert.variant, alert); + } + } + } + + Ok((map, disk_map)) +} diff --git a/bin/core/src/monitor/helpers.rs b/bin/core/src/monitor/helpers.rs new file mode 100644 index 000000000..91524548b --- /dev/null +++ b/bin/core/src/monitor/helpers.rs @@ -0,0 +1,118 @@ +use monitor_client::entities::{ + deployment::{Deployment, DockerContainerState}, + server::{ + stats::{ + ServerHealth, SeverityLevel, SingleDiskUsage, SystemStats, + }, + Server, ServerConfig, ServerStatus, + }, +}; +use serror::Serror; + +use crate::state::{deployment_status_cache, server_status_cache}; + +use super::{CachedDeploymentStatus, CachedServerStatus, History}; + +#[instrument(level = "debug", skip_all)] +pub async fn insert_deployments_status_unknown( + deployments: Vec, +) { + let status_cache = deployment_status_cache(); + for deployment in deployments { + let prev = + status_cache.get(&deployment.id).await.map(|s| s.curr.state); + status_cache + .insert( + deployment.id.clone(), + History { + curr: CachedDeploymentStatus { + id: deployment.id, + state: DockerContainerState::Unknown, + container: None, + }, + prev, + } + .into(), + ) + .await; + } +} + +#[instrument(level = "debug", skip_all)] +pub async fn insert_server_status( + server: &Server, + status: ServerStatus, + version: String, + stats: Option, + err: impl Into>, +) { + let health = stats.as_ref().map(|s| get_server_health(server, s)); + server_status_cache() + .insert( + server.id.clone(), + CachedServerStatus { + id: server.id.clone(), + status, + version, + stats, + health, + err: err.into(), + } + .into(), + ) + .await; +} + +fn get_server_health( + server: &Server, + SystemStats { + cpu_perc, + mem_used_gb, + mem_total_gb, + disks, + .. + }: &SystemStats, +) -> ServerHealth { + let ServerConfig { + cpu_warning, + cpu_critical, + mem_warning, + mem_critical, + disk_warning, + disk_critical, + .. + } = &server.config; + let mut health = ServerHealth::default(); + + if cpu_perc >= cpu_critical { + health.cpu = SeverityLevel::Critical + } else if cpu_perc >= cpu_warning { + health.cpu = SeverityLevel::Warning + } + + let mem_perc = 100.0 * mem_used_gb / mem_total_gb; + if mem_perc >= *mem_critical { + health.mem = SeverityLevel::Critical + } else if mem_perc >= *mem_warning { + health.mem = SeverityLevel::Warning + } + + for SingleDiskUsage { + mount, + used_gb, + total_gb, + } in disks + { + let perc = 100.0 * used_gb / total_gb; + let stats_state = if perc >= *disk_critical { + SeverityLevel::Critical + } else if perc >= *disk_warning { + SeverityLevel::Warning + } else { + SeverityLevel::Ok + }; + health.disks.insert(mount.clone(), stats_state); + } + + health +} diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs new file mode 100644 index 000000000..210eec3dd --- /dev/null +++ b/bin/core/src/monitor/mod.rs @@ -0,0 +1,189 @@ +use async_timing_util::{wait_until_timelength, Timelength}; +use futures::future::join_all; +use monitor_client::entities::{ + deployment::{ContainerSummary, DockerContainerState}, + server::{ + stats::{ServerHealth, SystemStats}, + Server, ServerStatus, + }, +}; +use mungos::{find::find_collect, mongodb::bson::doc}; +use periphery_client::api; +use serror::Serror; + +use crate::{ + helpers::periphery_client, + monitor::{alert::check_alerts, record::record_server_stats}, + state::{db_client, deployment_status_cache}, +}; + +use self::helpers::{ + insert_deployments_status_unknown, insert_server_status, +}; + +mod alert; +mod helpers; +mod record; + +#[derive(Default, Debug)] +pub struct History { + pub curr: Curr, + pub prev: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct CachedServerStatus { + pub id: String, + pub status: ServerStatus, + pub version: String, + pub stats: Option, + pub health: Option, + pub err: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct CachedDeploymentStatus { + pub id: String, + pub state: DockerContainerState, + pub container: Option, +} + +pub fn spawn_monitor_loop() { + tokio::spawn(async move { + loop { + let ts = (wait_until_timelength(Timelength::FiveSeconds, 500) + .await + - 500) as i64; + let servers = + match find_collect(&db_client().await.servers, None, None) + .await + { + Ok(servers) => servers, + Err(e) => { + error!( + "failed to get server list (manage status cache) | {e:#}" + ); + continue; + } + }; + let futures = servers.into_iter().map(|server| async move { + update_cache_for_server(&server).await; + }); + join_all(futures).await; + tokio::join!(check_alerts(ts), record_server_stats(ts)); + } + }); +} + +#[instrument(level = "debug")] +pub async fn update_cache_for_server(server: &Server) { + let deployments = match find_collect( + &db_client().await.deployments, + doc! { "config.server_id": &server.id }, + None, + ) + .await + { + Ok(deployments) => deployments, + Err(e) => { + error!("failed to get deployments list from mongo (update status cache) | server id: {} | {e:#}", server.id); + return; + } + }; + + if !server.config.enabled { + insert_deployments_status_unknown(deployments).await; + insert_server_status( + server, + ServerStatus::Disabled, + String::from("unknown"), + None, + None, + ) + .await; + return; + } + // already handle server disabled case above, so using unwrap here + let periphery = periphery_client(server).unwrap(); + + let version = match periphery.request(api::GetVersion {}).await { + Ok(version) => version.version, + Err(e) => { + insert_deployments_status_unknown(deployments).await; + insert_server_status( + server, + ServerStatus::NotOk, + String::from("unknown"), + None, + Serror::from(&e), + ) + .await; + return; + } + }; + + let stats = if server.config.stats_monitoring { + match periphery.request(api::stats::GetSystemStats {}).await { + Ok(stats) => Some(stats), + Err(e) => { + insert_deployments_status_unknown(deployments).await; + insert_server_status( + server, + ServerStatus::NotOk, + String::from("unknown"), + None, + Serror::from(&e), + ) + .await; + return; + } + } + } else { + None + }; + + insert_server_status( + server, + ServerStatus::Ok, + version, + stats, + None, + ) + .await; + + let containers = + periphery.request(api::container::GetContainerList {}).await; + if containers.is_err() { + insert_deployments_status_unknown(deployments).await; + return; + } + + let containers = containers.unwrap(); + let status_cache = deployment_status_cache(); + for deployment in deployments { + let container = containers + .iter() + .find(|c| c.name == deployment.name) + .cloned(); + let prev = + status_cache.get(&deployment.id).await.map(|s| s.curr.state); + let state = container + .as_ref() + .map(|c| c.state) + .unwrap_or(DockerContainerState::NotDeployed); + status_cache + .insert( + deployment.id.clone(), + History { + curr: CachedDeploymentStatus { + id: deployment.id, + state, + container, + }, + prev, + } + .into(), + ) + .await; + } +} diff --git a/bin/core/src/monitor/record.rs b/bin/core/src/monitor/record.rs new file mode 100644 index 000000000..c046bacf9 --- /dev/null +++ b/bin/core/src/monitor/record.rs @@ -0,0 +1,39 @@ +use monitor_client::entities::server::stats::{ + sum_disk_usage, SystemStatsRecord, TotalDiskUsage, +}; + +use crate::state::{db_client, server_status_cache}; + +#[instrument(level = "debug")] +pub async fn record_server_stats(ts: i64) { + let status = server_status_cache().get_list().await; + let records = status + .into_iter() + .filter_map(|status| { + let stats = status.stats.as_ref()?; + + let TotalDiskUsage { + used_gb: disk_used_gb, + total_gb: disk_total_gb, + } = sum_disk_usage(&stats.disks); + + Some(SystemStatsRecord { + ts, + sid: status.id.clone(), + cpu_perc: stats.cpu_perc, + mem_total_gb: stats.mem_total_gb, + mem_used_gb: stats.mem_used_gb, + disk_total_gb, + disk_used_gb, + disks: stats.disks.clone(), + }) + }) + .collect::>(); + if !records.is_empty() { + let res = + db_client().await.stats.insert_many(records, None).await; + if let Err(e) = res { + error!("failed to record server stats | {e:#}"); + } + } +} diff --git a/bin/core/src/state.rs b/bin/core/src/state.rs new file mode 100644 index 000000000..dfede010c --- /dev/null +++ b/bin/core/src/state.rs @@ -0,0 +1,54 @@ +use std::sync::{Arc, OnceLock}; + +use monitor_client::entities::deployment::DockerContainerState; +use tokio::sync::OnceCell; + +use crate::{ + auth::jwt::JwtClient, + config::core_config, + db::DbClient, + helpers::{action_state::ActionStates, cache::Cache}, + monitor::{CachedDeploymentStatus, CachedServerStatus, History}, +}; + +pub struct State; + +pub async fn db_client() -> &'static DbClient { + static DB_CLIENT: OnceCell = OnceCell::const_new(); + DB_CLIENT + .get_or_init(|| async { + DbClient::new(&core_config().mongo) + .await + .expect("failed to initialize mongo client") + }) + .await +} + +pub fn jwt_client() -> &'static JwtClient { + static JWT_CLIENT: OnceLock = OnceLock::new(); + JWT_CLIENT.get_or_init(|| JwtClient::new(core_config())) +} + +pub fn action_states() -> &'static ActionStates { + static ACTION_STATES: OnceLock = OnceLock::new(); + ACTION_STATES.get_or_init(ActionStates::default) +} + +pub type DeploymentStatusCache = Cache< + String, + Arc>, +>; + +pub fn deployment_status_cache() -> &'static DeploymentStatusCache { + static DEPLOYMENT_STATUS_CACHE: OnceLock = + OnceLock::new(); + DEPLOYMENT_STATUS_CACHE.get_or_init(Default::default) +} + +pub type ServerStatusCache = Cache>; + +pub fn server_status_cache() -> &'static ServerStatusCache { + static SERVER_STATUS_CACHE: OnceLock = + OnceLock::new(); + SERVER_STATUS_CACHE.get_or_init(Default::default) +} diff --git a/bin/core/src/ws.rs b/bin/core/src/ws.rs new file mode 100644 index 000000000..b4702a3f3 --- /dev/null +++ b/bin/core/src/ws.rs @@ -0,0 +1,218 @@ +use anyhow::{anyhow, Context}; +use axum::{ + extract::{ + ws::{Message, WebSocket}, + WebSocketUpgrade, + }, + response::IntoResponse, + routing::get, + Router, +}; +use futures::{SinkExt, StreamExt}; +use monitor_client::{ + entities::{ + permission::PermissionLevel, update::ResourceTarget, user::User, + }, + ws::WsLoginMessage, +}; +use mungos::by_id::find_one_by_id; +use serde_json::json; +use serror::serialize_error; +use tokio::select; +use tokio_util::sync::CancellationToken; + +use crate::{ + auth::{auth_api_key_check_enabled, auth_jwt_check_enabled}, + db::DbClient, + helpers::{ + channel::update_channel, + resource::get_user_permission_on_resource, + }, + state::db_client, +}; + +pub fn router() -> Router { + Router::new().route("/update", get(ws_handler)) +} + +#[instrument(level = "debug")] +async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { + // get a reveiver for internal update messages. + let mut receiver = update_channel().receiver.resubscribe(); + + // handle http -> ws updgrade + ws.on_upgrade(|socket| async move { + let Some((socket, user)) = ws_login(socket).await else { + return + }; + + let (mut ws_sender, mut ws_reciever) = socket.split(); + + let cancel = CancellationToken::new(); + let cancel_clone = cancel.clone(); + + tokio::spawn(async move { + let db_client = db_client().await; + loop { + // poll for updates off the receiver / await cancel. + let update = select! { + _ = cancel_clone.cancelled() => break, + update = receiver.recv() => {update.expect("failed to recv update msg")} + }; + + // before sending every update, verify user is still valid. + // kill the connection is user if found to be invalid. + let user = check_user_valid(db_client, &user.id).await; + let user = match user { + Err(e) => { + let _ = ws_sender + .send(Message::Text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string())) + .await; + let _ = ws_sender.close().await; + return; + }, + Ok(user) => user, + }; + + // Only send if user has permission on the target resource. + if user_can_see_update(&user, &update.target).await.is_ok() { + let _ = ws_sender + .send(Message::Text(serde_json::to_string(&update).unwrap())) + .await; + } + } + }); + + // Handle messages from the client. + // After login, only handles close message. + while let Some(msg) = ws_reciever.next().await { + match msg { + Ok(msg) => { + if let Message::Close(_) = msg { + cancel.cancel(); + return; + } + } + Err(_) => { + cancel.cancel(); + return; + } + } + } + }) +} + +#[instrument(level = "debug")] +async fn ws_login( + mut socket: WebSocket, +) -> Option<(WebSocket, User)> { + let login_msg = match socket.recv().await { + Some(Ok(Message::Text(login_msg))) => LoginMessage::Ok(login_msg), + Some(Ok(msg)) => { + LoginMessage::Err(format!("invalid login message: {msg:?}")) + } + Some(Err(e)) => { + LoginMessage::Err(format!("failed to get login message: {e:?}")) + } + None => { + LoginMessage::Err("failed to get login message".to_string()) + } + }; + let login_msg = match login_msg { + LoginMessage::Ok(login_msg) => login_msg, + LoginMessage::Err(msg) => { + let _ = socket.send(Message::Text(msg)).await; + let _ = socket.close().await; + return None; + } + }; + match WsLoginMessage::from_json_str(&login_msg) { + // Login using a jwt + Ok(WsLoginMessage::Jwt { jwt }) => { + match auth_jwt_check_enabled(&jwt).await { + Ok(user) => { + let _ = + socket.send(Message::Text("LOGGED_IN".to_string())).await; + Some((socket, user)) + } + Err(e) => { + let _ = socket + .send(Message::Text(format!( + "failed to authenticate user using jwt | {e:#}" + ))) + .await; + let _ = socket.close().await; + None + } + } + } + // login using api keys + Ok(WsLoginMessage::ApiKeys { key, secret }) => { + match auth_api_key_check_enabled(&key, &secret).await { + Ok(user) => { + let _ = + socket.send(Message::Text("LOGGED_IN".to_string())).await; + Some((socket, user)) + } + Err(e) => { + let _ = socket + .send(Message::Text(format!( + "failed to authenticate user using api keys | {e:#}" + ))) + .await; + let _ = socket.close().await; + None + } + } + } + Err(e) => { + let _ = socket + .send(Message::Text(format!( + "failed to parse login message: {e:#}" + ))) + .await; + let _ = socket.close().await; + None + } + } +} + +enum LoginMessage { + /// The text message + Ok(String), + /// The err message + Err(String), +} + +#[instrument(level = "debug", skip(db_client))] +async fn check_user_valid( + db_client: &DbClient, + user_id: &str, +) -> anyhow::Result { + let user = find_one_by_id(&db_client.users, user_id) + .await + .context("failed to query mongo for users")? + .context("user not found")?; + if !user.enabled { + return Err(anyhow!("user not enabled")); + } + Ok(user) +} + +#[instrument(level = "debug")] +async fn user_can_see_update( + user: &User, + update_target: &ResourceTarget, +) -> anyhow::Result<()> { + if user.admin { + return Ok(()); + } + let (variant, id) = update_target.extract_variant_id(); + let permissions = + get_user_permission_on_resource(&user.id, variant, id).await?; + if permissions > PermissionLevel::None { + Ok(()) + } else { + Err(anyhow!("user does not have permissions on {variant} {id}")) + } +} diff --git a/bin/migrator/Cargo.toml b/bin/migrator/Cargo.toml new file mode 100644 index 000000000..36b5e85ad --- /dev/null +++ b/bin/migrator/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "migrator" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +monitor_client.workspace = true +# db_client.workspace = true +logger.workspace = true +# +termination_signal.workspace = true +mungos.workspace = true +mongo_indexed.workspace = true +# +tokio.workspace = true +anyhow.workspace = true +dotenv.workspace = true +envy.workspace = true +serde.workspace = true +tracing.workspace = true +chrono = "0.4" \ No newline at end of file diff --git a/bin/migrator/src/legacy/mod.rs b/bin/migrator/src/legacy/mod.rs new file mode 100644 index 000000000..2d24cd45f --- /dev/null +++ b/bin/migrator/src/legacy/mod.rs @@ -0,0 +1 @@ +pub mod v0; diff --git a/bin/migrator/src/legacy/v0/action.rs b/bin/migrator/src/legacy/v0/action.rs new file mode 100644 index 000000000..f133f0796 --- /dev/null +++ b/bin/migrator/src/legacy/v0/action.rs @@ -0,0 +1,53 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::{Command, PermissionsMap}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Action { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, + + #[serde(default)] + pub description: String, + + pub path: String, + + pub command: String, + + // run action on all servers in this array + #[serde(default)] + pub server_ids: Vec, + + // run action on all servers in these groups + #[serde(default)] + pub group_ids: Vec, + + // run action on all servers + #[serde(default)] + pub run_on_all: bool, + + #[serde(default)] + pub permissions: PermissionsMap, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +impl From for Command { + fn from(value: Action) -> Command { + Command { + path: value.path, + command: value.command, + } + } +} diff --git a/bin/migrator/src/legacy/v0/alert.rs b/bin/migrator/src/legacy/v0/alert.rs new file mode 100644 index 000000000..3fd1ad0da --- /dev/null +++ b/bin/migrator/src/legacy/v0/alert.rs @@ -0,0 +1,13 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Alert { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, +} diff --git a/bin/migrator/src/legacy/v0/build.rs b/bin/migrator/src/legacy/v0/build.rs new file mode 100644 index 000000000..7c759272d --- /dev/null +++ b/bin/migrator/src/legacy/v0/build.rs @@ -0,0 +1,234 @@ +use anyhow::{anyhow, Context}; +use monitor_client::entities::build::{BuildConfig, BuildInfo}; +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::{ + unix_from_monitor_ts, Command, EnvironmentVar, PermissionsMap, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Build { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, + + #[serde(default)] + pub description: String, + + #[serde(default)] + pub permissions: PermissionsMap, + + #[serde(default)] + pub skip_secret_interp: bool, + + pub server_id: Option, // server which this image should be built on + + pub aws_config: Option, + + pub version: Version, + + // git related + pub repo: Option, + + pub branch: Option, + + pub github_account: Option, + + // build related + pub pre_build: Option, + + pub docker_build_args: Option, + + pub docker_account: Option, + + pub docker_organization: Option, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub last_built_at: String, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildActionState { + pub building: bool, + pub updating: bool, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct Version { + pub major: i32, + pub minor: i32, + pub patch: i32, +} + +impl ToString for Version { + fn to_string(&self) -> String { + format!("{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl TryFrom<&str> for Version { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let vals = value + .split('.') + .map(|v| { + anyhow::Ok( + v.parse().context("failed at parsing value into i32")?, + ) + }) + .collect::>>()?; + let version = Version { + major: *vals + .first() + .ok_or(anyhow!("must include at least major version"))?, + minor: *vals.get(1).unwrap_or(&0), + patch: *vals.get(2).unwrap_or(&0), + }; + Ok(version) + } +} + +impl Version { + pub fn increment(&mut self) { + self.patch += 1; + } +} + +impl From for monitor_client::entities::Version { + fn from(value: Version) -> Self { + Self { + major: value.major, + minor: value.minor, + patch: value.patch, + } + } +} + +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Default, +)] +pub struct DockerBuildArgs { + pub build_path: String, + pub dockerfile_path: Option, + #[serde(default)] + pub build_args: Vec, + #[serde(default)] + pub extra_args: Vec, + #[serde(default)] + pub use_buildx: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildVersionsReponse { + pub version: Version, + pub ts: String, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Default, +)] +pub struct AwsBuilderBuildConfig { + pub region: Option, + + pub instance_type: Option, + + pub ami_name: Option, + + pub volume_gb: Option, + + pub subnet_id: Option, + + pub security_group_ids: Option>, + + pub key_pair_name: Option, + + pub assign_public_ip: Option, +} + +impl TryFrom for monitor_client::entities::build::Build { + type Error = anyhow::Error; + fn try_from(value: Build) -> Result { + let ( + build_path, + dockerfile_path, + build_args, + extra_args, + use_buildx, + ) = value + .docker_build_args + .map(|args| { + ( + args.build_path, + args.dockerfile_path.unwrap_or_default(), + args + .build_args + .into_iter() + .map(|arg| monitor_client::entities::EnvironmentVar { + variable: arg.variable, + value: arg.value, + }) + .collect::>(), + args.extra_args, + args.use_buildx, + ) + }) + .unwrap_or_default(); + + let build = Self { + id: value.id, + name: value.name, + description: value.description, + // permissions: value + // .permissions + // .into_iter() + // .map(|(id, p)| (id, p.into())) + // .collect(), + updated_at: unix_from_monitor_ts(&value.updated_at)?, + tags: Vec::new(), + info: BuildInfo { + last_built_at: unix_from_monitor_ts(&value.last_built_at)?, + }, + config: BuildConfig { + builder_id: String::new(), + skip_secret_interp: value.skip_secret_interp, + version: value.version.into(), + repo: value.repo.unwrap_or_default(), + branch: value.branch.unwrap_or_default(), + github_account: value.github_account.unwrap_or_default(), + docker_account: value.docker_account.unwrap_or_default(), + docker_organization: value + .docker_organization + .unwrap_or_default(), + pre_build: value + .pre_build + .map(|command| monitor_client::entities::SystemCommand { + path: command.path, + command: command.command, + }) + .unwrap_or_default(), + build_path, + dockerfile_path, + build_args, + extra_args, + use_buildx, + labels: Default::default(), + }, + }; + Ok(build) + } +} diff --git a/bin/migrator/src/legacy/v0/config.rs b/bin/migrator/src/legacy/v0/config.rs new file mode 100644 index 000000000..027a7d261 --- /dev/null +++ b/bin/migrator/src/legacy/v0/config.rs @@ -0,0 +1,201 @@ +use std::{collections::HashMap, net::IpAddr, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use super::Timelength; + +pub type GithubUsername = String; +pub type GithubToken = String; +pub type GithubAccounts = HashMap; + +pub type DockerUsername = String; +pub type DockerToken = String; +pub type DockerAccounts = HashMap; + +pub type SecretsMap = HashMap; // these are used for injection into deployments run commands + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoreConfig { + #[serde(default = "default_title")] + pub title: String, + + // the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech' + pub host: String, + + // port the core web server runs on + #[serde(default = "default_core_port")] + pub port: u16, + + pub jwt_secret: String, + + #[serde(default = "default_jwt_valid_for")] + pub jwt_valid_for: Timelength, + + // interval at which to collect server stats and alert for out of bounds + pub monitoring_interval: Timelength, + + // daily utc offset in hours to run daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight. + #[serde(default)] + pub daily_offset_hours: u8, + + // number of days to keep stats, or 0 to disable pruning. stats older than this number of days are deleted on a daily cycle + #[serde(default)] + pub keep_stats_for_days: u64, + + // used to verify validity from github webhooks + pub github_webhook_secret: String, + + // used to form the frontend listener url, if None will use 'host'. + pub github_webhook_base_url: Option, + + // sent in auth header with req to periphery + pub passkey: String, + + // integration with slack app + pub slack_url: Option, + + // enable login with local auth + pub local_auth: bool, + + // allowed docker orgs used with monitor. first in this list will be default for build + #[serde(default)] + pub docker_organizations: Vec, + + pub mongo: MongoConfig, + + #[serde(default)] + pub github_oauth: OauthCredentials, + + #[serde(default)] + pub google_oauth: OauthCredentials, + + #[serde(default)] + pub aws: AwsBuilderConfig, +} + +fn default_title() -> String { + String::from("monitor") +} + +fn default_core_port() -> u16 { + 9000 +} + +fn default_jwt_valid_for() -> Timelength { + Timelength::OneWeek +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct OauthCredentials { + #[serde(default)] + pub enabled: bool, + #[serde(default)] + pub id: String, + #[serde(default)] + pub secret: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MongoConfig { + pub uri: String, + #[serde(default = "default_core_mongo_app_name")] + pub app_name: String, + #[serde(default = "default_core_mongo_db_name")] + pub db_name: String, +} + +fn default_core_mongo_app_name() -> String { + "monitor_core".to_string() +} + +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_name: 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, + + #[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; // (ami_name, AmiAccounts) + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct AmiAccounts { + pub ami_id: String, + #[serde(default)] + pub github: Vec, + #[serde(default)] + pub docker: Vec, + #[serde(default)] + pub secrets: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PeripheryConfig { + #[serde(default = "default_periphery_port")] + pub port: u16, + #[serde(default = "default_repo_dir")] + pub repo_dir: PathBuf, + #[serde(default = "default_stats_refresh_interval")] + pub stats_polling_rate: Timelength, + #[serde(default)] + pub allowed_ips: Vec, + #[serde(default)] + pub passkeys: Vec, + #[serde(default)] + pub secrets: SecretsMap, + #[serde(default)] + pub github_accounts: GithubAccounts, + #[serde(default)] + pub docker_accounts: DockerAccounts, +} + +fn default_periphery_port() -> u16 { + 8000 +} + +fn default_repo_dir() -> PathBuf { + "/repos".parse().unwrap() +} + +fn default_stats_refresh_interval() -> Timelength { + Timelength::FiveSeconds +} diff --git a/bin/migrator/src/legacy/v0/deployment.rs b/bin/migrator/src/legacy/v0/deployment.rs new file mode 100644 index 000000000..636bbce6d --- /dev/null +++ b/bin/migrator/src/legacy/v0/deployment.rs @@ -0,0 +1,386 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use crate::legacy::v0::unix_from_monitor_ts; + +use super::{Command, EnvironmentVar, PermissionsMap, Version}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Deployment { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, // must be formatted to be compat with docker + + #[serde(default)] + pub description: String, + + pub server_id: String, + + #[serde(default)] + pub permissions: PermissionsMap, + + #[serde(default)] + pub skip_secret_interp: bool, + + pub docker_run_args: DockerRunArgs, + + #[serde(default = "default_term_signal_labels")] + pub term_signal_labels: Vec, + + #[serde(default)] + pub termination_signal: TerminationSignal, + + #[serde(default = "default_termination_timeout")] + pub termination_timeout: i32, + + pub build_id: Option, + + #[serde(default)] + pub redeploy_on_build: bool, + + pub build_version: Option, + + // deployment repo related + pub repo: Option, + + pub branch: Option, + + pub github_account: Option, + + pub on_clone: Option, + + pub on_pull: Option, + + pub repo_mount: Option, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +fn default_termination_timeout() -> i32 { + 10 +} + +fn default_term_signal_labels() -> Vec { + vec![TerminationSignalLabel::default()] +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct DeploymentWithContainerState { + pub deployment: Deployment, + pub state: DockerContainerState, + pub container: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct DeploymentActionState { + pub deploying: bool, + pub stopping: bool, + pub starting: bool, + pub removing: bool, + pub pulling: bool, + pub recloning: bool, + pub updating: bool, + pub renaming: bool, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, +)] +pub struct TerminationSignalLabel { + pub signal: TerminationSignal, + pub label: String, +} + +impl From + for monitor_client::entities::deployment::TerminationSignalLabel +{ + fn from(value: TerminationSignalLabel) -> Self { + Self { + signal: value.signal.into(), + label: value.label, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DockerRunArgs { + pub image: String, + + #[serde(default)] + pub ports: Vec, + + #[serde(default)] + pub volumes: Vec, + + #[serde(default)] + pub environment: Vec, + + #[serde(default = "default_network")] + pub network: String, + + #[serde(default)] + pub restart: RestartMode, + + pub post_image: Option, + + pub container_user: Option, + + #[serde(default)] + pub extra_args: Vec, + + pub docker_account: Option, // the username of the dockerhub account +} + +impl Default for DockerRunArgs { + fn default() -> DockerRunArgs { + DockerRunArgs { + network: "host".to_string(), + image: Default::default(), + ports: Default::default(), + volumes: Default::default(), + environment: Default::default(), + restart: Default::default(), + post_image: Default::default(), + container_user: Default::default(), + extra_args: Default::default(), + docker_account: Default::default(), + } + } +} + +fn default_network() -> String { + String::from("host") +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BasicContainerInfo { + pub name: String, + pub id: String, + pub image: String, + pub state: DockerContainerState, + pub status: Option, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct Conversion { + pub local: String, + pub container: String, +} + +impl From + for monitor_client::entities::deployment::Conversion +{ + fn from(value: Conversion) -> Self { + Self { + local: value.local, + container: value.container, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DockerContainerStats { + #[serde(alias = "Name")] + pub name: String, + #[serde(alias = "CPUPerc")] + pub cpu_perc: String, + #[serde(alias = "MemPerc")] + pub mem_perc: String, + #[serde(alias = "MemUsage")] + pub mem_usage: String, + #[serde(alias = "NetIO")] + pub net_io: String, + #[serde(alias = "BlockIO")] + pub block_io: String, + #[serde(alias = "PIDs")] + pub pids: String, +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum DockerContainerState { + #[default] + Unknown, + NotDeployed, + Created, + Restarting, + Running, + Removing, + Paused, + Exited, + Dead, +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +pub enum RestartMode { + #[default] + #[serde(rename = "no")] + NoRestart, + #[serde(rename = "on-failure")] + OnFailure, + #[serde(rename = "always")] + Always, + #[serde(rename = "unless-stopped")] + UnlessStopped, +} + +impl From + for monitor_client::entities::deployment::RestartMode +{ + fn from(value: RestartMode) -> Self { + use monitor_client::entities::deployment::RestartMode::*; + match value { + RestartMode::NoRestart => NoRestart, + RestartMode::OnFailure => OnFailure, + RestartMode::Always => Always, + RestartMode::UnlessStopped => UnlessStopped, + } + } +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "UPPERCASE")] +#[allow(clippy::enum_variant_names)] +pub enum TerminationSignal { + #[serde(alias = "1")] + SigHup, + #[serde(alias = "2")] + SigInt, + #[serde(alias = "3")] + SigQuit, + #[default] + #[serde(alias = "15")] + SigTerm, +} + +impl From + for monitor_client::entities::deployment::TerminationSignal +{ + fn from(value: TerminationSignal) -> Self { + use monitor_client::entities::deployment::TerminationSignal::*; + match value { + TerminationSignal::SigHup => SigHup, + TerminationSignal::SigInt => SigInt, + TerminationSignal::SigQuit => SigQuit, + TerminationSignal::SigTerm => SigTerm, + } + } +} + +impl TryFrom + for monitor_client::entities::deployment::Deployment +{ + type Error = anyhow::Error; + fn try_from(value: Deployment) -> Result { + let image = if let Some(build_id) = value.build_id { + monitor_client::entities::deployment::DeploymentImage::Build { + build_id, + version: value.build_version.unwrap_or_default().into(), + } + } else { + monitor_client::entities::deployment::DeploymentImage::Image { + image: value.docker_run_args.image, + } + }; + let deployment = Self { + id: value.id, + name: value.name, + description: value.description, + // permissions: value + // .permissions + // .into_iter() + // .map(|(id, p)| (id, p.into())) + // .collect(), + updated_at: unix_from_monitor_ts(&value.updated_at)?, + tags: Vec::new(), + info: (), + config: + monitor_client::entities::deployment::DeploymentConfig { + server_id: value.server_id, + send_alerts: true, + image, + skip_secret_interp: value.skip_secret_interp, + redeploy_on_build: value.redeploy_on_build, + term_signal_labels: value + .term_signal_labels + .into_iter() + .map(|t| t.into()) + .collect(), + termination_signal: value.termination_signal.into(), + termination_timeout: value.termination_timeout, + ports: value + .docker_run_args + .ports + .into_iter() + .map(|p| p.into()) + .collect(), + volumes: value + .docker_run_args + .volumes + .into_iter() + .map(|v| v.into()) + .collect(), + environment: value + .docker_run_args + .environment + .into_iter() + .map(|e| e.into()) + .collect(), + network: value.docker_run_args.network, + restart: value.docker_run_args.restart.into(), + process_args: value + .docker_run_args + .post_image + .unwrap_or_default(), + extra_args: value.docker_run_args.extra_args, + docker_account: value + .docker_run_args + .docker_account + .unwrap_or_default(), + labels: Default::default(), + }, + }; + Ok(deployment) + } +} diff --git a/bin/migrator/src/legacy/v0/group.rs b/bin/migrator/src/legacy/v0/group.rs new file mode 100644 index 000000000..7f53759e9 --- /dev/null +++ b/bin/migrator/src/legacy/v0/group.rs @@ -0,0 +1,38 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::PermissionsMap; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Group { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, + + #[serde(default)] + pub description: String, + + #[serde(default)] + pub permissions: PermissionsMap, + + pub builds: Vec, + + pub deployments: Vec, + + pub servers: Vec, + + pub procedures: Vec, + + pub groups: Vec, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} diff --git a/bin/migrator/src/legacy/v0/mod.rs b/bin/migrator/src/legacy/v0/mod.rs new file mode 100644 index 000000000..523b4563a --- /dev/null +++ b/bin/migrator/src/legacy/v0/mod.rs @@ -0,0 +1,336 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, Context}; +use chrono::{DateTime, LocalResult, SecondsFormat, TimeZone, Utc}; +use serde::{Deserialize, Serialize}; + +pub mod traits; + +mod action; +mod alert; +mod build; +mod config; +mod deployment; +mod group; +mod periphery_command; +mod procedure; +mod server; +mod update; +mod user; + +pub use action::*; +pub use alert::*; +pub use build::*; +pub use config::*; +pub use deployment::*; +pub use group::*; +pub use periphery_command::*; +pub use procedure::*; +pub use server::*; +pub use update::*; +pub use user::*; + +pub type PermissionsMap = HashMap; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CloneArgs { + pub name: String, + pub repo: Option, + pub branch: Option, + pub on_clone: Option, + pub on_pull: Option, + pub github_account: Option, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct Command { + #[serde(default)] + pub path: String, + #[serde(default)] + pub command: String, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct EnvironmentVar { + pub variable: String, + pub value: String, +} + +impl From + for monitor_client::entities::EnvironmentVar +{ + fn from(value: EnvironmentVar) -> Self { + Self { + variable: value.variable, + value: value.value, + } + } +} + +#[derive(Deserialize, Debug)] +pub struct UserCredentials { + pub username: String, + pub password: String, +} + +#[derive( + Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, Copy, +)] +#[serde(rename_all = "snake_case")] +pub enum AccountType { + Github, + Docker, +} + +#[derive( + Serialize, + Deserialize, + Debug, + Default, + PartialEq, + Hash, + Eq, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +pub enum Operation { + // do nothing + #[default] + None, + + // server + CreateServer, + UpdateServer, + DeleteServer, + PruneImagesServer, + PruneContainersServer, + PruneNetworksServer, + RenameServer, + + // build + CreateBuild, + UpdateBuild, + DeleteBuild, + BuildBuild, + + // deployment + CreateDeployment, + UpdateDeployment, + DeleteDeployment, + DeployContainer, + StopContainer, + StartContainer, + RemoveContainer, + PullDeployment, + RecloneDeployment, + RenameDeployment, + + // procedure + CreateProcedure, + UpdateProcedure, + DeleteProcedure, + + // command + CreateCommand, + UpdateCommand, + DeleteCommand, + RunCommand, + + // group + CreateGroup, + UpdateGroup, + DeleteGroup, + + // user + ModifyUserEnabled, + ModifyUserCreateServerPermissions, + ModifyUserCreateBuildPermissions, + ModifyUserPermissions, + + // github webhook automation + AutoBuild, + AutoPull, +} + +impl From for monitor_client::entities::Operation { + fn from(value: Operation) -> Self { + use monitor_client::entities::Operation::*; + match value { + Operation::None => None, + Operation::CreateServer => CreateServer, + Operation::UpdateServer => UpdateServer, + Operation::DeleteServer => DeleteServer, + Operation::PruneImagesServer => PruneImagesServer, + Operation::PruneContainersServer => PruneContainersServer, + Operation::PruneNetworksServer => PruneNetworksServer, + Operation::RenameServer => RenameServer, + Operation::CreateBuild => CreateBuild, + Operation::UpdateBuild => UpdateBuild, + Operation::DeleteBuild => DeleteBuild, + Operation::BuildBuild => RunBuild, + Operation::CreateDeployment => CreateDeployment, + Operation::UpdateDeployment => UpdateDeployment, + Operation::DeleteDeployment => DeleteDeployment, + Operation::DeployContainer => DeployContainer, + Operation::StopContainer => StopContainer, + Operation::StartContainer => StartContainer, + Operation::RemoveContainer => RemoveContainer, + Operation::PullDeployment => None, + Operation::RecloneDeployment => None, + Operation::RenameDeployment => RenameDeployment, + Operation::CreateProcedure => None, + Operation::UpdateProcedure => None, + Operation::DeleteProcedure => None, + Operation::CreateCommand => None, + Operation::UpdateCommand => None, + Operation::DeleteCommand => None, + Operation::RunCommand => None, + Operation::CreateGroup => None, + Operation::UpdateGroup => None, + Operation::DeleteGroup => None, + Operation::ModifyUserEnabled => None, + Operation::ModifyUserCreateServerPermissions => None, + Operation::ModifyUserCreateBuildPermissions => None, + Operation::ModifyUserPermissions => None, + Operation::AutoBuild => RunBuild, + Operation::AutoPull => PullRepo, + } + } +} + +#[derive( + Serialize, + Deserialize, + Debug, + Hash, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum PermissionLevel { + #[default] + None, + Read, + Execute, + Update, +} + +impl Default for &PermissionLevel { + fn default() -> Self { + &PermissionLevel::None + } +} + +impl From + for monitor_client::entities::permission::PermissionLevel +{ + fn from(value: PermissionLevel) -> Self { + use monitor_client::entities::permission::PermissionLevel::*; + match value { + PermissionLevel::None => None, + PermissionLevel::Read => Read, + PermissionLevel::Execute => Execute, + PermissionLevel::Update => Write, + } + } +} + +#[derive( + Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, Copy, +)] +#[serde(rename_all = "snake_case")] +pub enum PermissionsTarget { + Server, + Deployment, + Build, + Procedure, + Group, +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum Timelength { + #[serde(rename = "1-sec")] + OneSecond, + #[serde(rename = "5-sec")] + FiveSeconds, + #[serde(rename = "10-sec")] + TenSeconds, + #[serde(rename = "15-sec")] + FifteenSeconds, + #[serde(rename = "30-sec")] + ThirtySeconds, + #[default] + #[serde(rename = "1-min")] + OneMinute, + #[serde(rename = "2-min")] + TwoMinutes, + #[serde(rename = "5-min")] + FiveMinutes, + #[serde(rename = "10-min")] + TenMinutes, + #[serde(rename = "15-min")] + FifteenMinutes, + #[serde(rename = "30-min")] + ThirtyMinutes, + #[serde(rename = "1-hr")] + OneHour, + #[serde(rename = "2-hr")] + TwoHours, + #[serde(rename = "6-hr")] + SixHours, + #[serde(rename = "8-hr")] + EightHours, + #[serde(rename = "12-hr")] + TwelveHours, + #[serde(rename = "1-day")] + OneDay, + #[serde(rename = "3-day")] + ThreeDay, + #[serde(rename = "1-wk")] + OneWeek, + #[serde(rename = "2-wk")] + TwoWeeks, + #[serde(rename = "30-day")] + ThirtyDays, +} + +pub fn unix_from_monitor_ts(ts: &str) -> anyhow::Result { + Ok( + DateTime::parse_from_rfc3339(ts) + .context("failed to parse rfc3339 timestamp")? + .timestamp_millis(), + ) +} + +pub fn monitor_ts_from_unix(ts: i64) -> anyhow::Result { + match Utc.timestamp_millis_opt(ts) { + LocalResult::Single(dt) => { + Ok(dt.to_rfc3339_opts(SecondsFormat::Millis, false)) + } + LocalResult::None => { + Err(anyhow!("out of bounds timestamp passed")) + } + _ => unreachable!(), + } +} diff --git a/bin/migrator/src/legacy/v0/periphery_command.rs b/bin/migrator/src/legacy/v0/periphery_command.rs new file mode 100644 index 000000000..013d09f79 --- /dev/null +++ b/bin/migrator/src/legacy/v0/periphery_command.rs @@ -0,0 +1,38 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::{Command, PermissionsMap}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct PeripheryCommand { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, // must be formatted to be compat with docker + + #[serde(default)] + pub description: String, + + pub server_id: String, + + #[serde(default)] + pub permissions: PermissionsMap, + + #[serde(default)] + pub command: Command, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct CommandActionState { + pub running: bool, +} diff --git a/bin/migrator/src/legacy/v0/procedure.rs b/bin/migrator/src/legacy/v0/procedure.rs new file mode 100644 index 000000000..64dfc3310 --- /dev/null +++ b/bin/migrator/src/legacy/v0/procedure.rs @@ -0,0 +1,79 @@ +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::PermissionsMap; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Procedure { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, + + #[serde(default)] + pub description: String, + + #[serde(default)] + pub stages: Vec, + + #[serde(default)] + pub webhook_branches: Vec, + + #[serde(default)] + pub permissions: PermissionsMap, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct ProcedureStage { + pub operation: ProcedureOperation, + pub target_id: String, +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum ProcedureOperation { + // do nothing + #[default] + None, + + // server + PruneImagesServer, + PruneContainersServer, + PruneNetworksServer, + + // build + BuildBuild, + + // deployment + DeployContainer, + StopContainer, + StartContainer, + RemoveContainer, + PullDeployment, + RecloneDeployment, + + // procedure + RunProcedure, +} diff --git a/bin/migrator/src/legacy/v0/server.rs b/bin/migrator/src/legacy/v0/server.rs new file mode 100644 index 000000000..e530a2f7d --- /dev/null +++ b/bin/migrator/src/legacy/v0/server.rs @@ -0,0 +1,324 @@ +use std::path::PathBuf; + +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::{unix_from_monitor_ts, PermissionsMap, Timelength}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Server { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub name: String, + + #[serde(default)] + pub description: String, + + pub address: String, + + #[serde(default)] + pub permissions: PermissionsMap, + + pub enabled: bool, + + #[serde(default)] + pub to_notify: Vec, // slack users to notify + + #[serde(default)] + pub auto_prune: bool, + + #[serde(default = "default_cpu_alert")] + pub cpu_alert: f32, + + #[serde(default = "default_mem_alert")] + pub mem_alert: f64, + + #[serde(default = "default_disk_alert")] + pub disk_alert: f64, + + #[serde(default)] + pub stats_interval: Timelength, + + pub region: Option, + + pub instance_id: Option, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +impl Default for Server { + fn default() -> Self { + Self { + id: Default::default(), + name: Default::default(), + address: Default::default(), + permissions: Default::default(), + enabled: true, + auto_prune: true, + to_notify: Default::default(), + cpu_alert: default_cpu_alert(), + mem_alert: default_mem_alert(), + disk_alert: default_disk_alert(), + stats_interval: Default::default(), + region: Default::default(), + instance_id: Default::default(), + description: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + } + } +} + +fn default_cpu_alert() -> f32 { + 95.0 +} + +fn default_mem_alert() -> f64 { + 80.0 +} + +fn default_disk_alert() -> f64 { + 75.0 +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ServerWithStatus { + pub server: Server, + pub status: ServerStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct ServerActionState { + pub pruning_networks: bool, + pub pruning_containers: bool, + pub pruning_images: bool, +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum ServerStatus { + Ok, + #[default] + NotOk, + Disabled, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct SystemStatsQuery { + #[serde(default)] + pub cpus: bool, + #[serde(default)] + pub disks: bool, + #[serde(default)] + pub networks: bool, + #[serde(default)] + pub components: bool, + #[serde(default)] + pub processes: bool, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct SystemStats { + #[serde(default)] + pub system_load: f64, + pub cpu_perc: f32, + pub cpu_freq_mhz: f64, + pub mem_used_gb: f64, // in GB + pub mem_total_gb: f64, // in GB + pub disk: DiskUsage, + #[serde(default)] + pub cpus: Vec, + #[serde(default)] + pub networks: Vec, + #[serde(default)] + pub components: Vec, + #[serde(default)] + pub processes: Vec, + pub polling_rate: Timelength, + pub refresh_ts: u128, + pub refresh_list_ts: u128, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SingleCpuUsage { + pub name: String, + pub usage: f32, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct DiskUsage { + pub used_gb: f64, // in GB + pub total_gb: f64, // in GB + pub read_kb: f64, // in kB + pub write_kb: f64, // in kB + #[serde(default)] + pub disks: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SingleDiskUsage { + pub mount: PathBuf, + pub used_gb: f64, // in GB + pub total_gb: f64, // in GB +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemNetwork { + pub name: String, + pub recieved_kb: f64, // in kB + pub transmitted_kb: f64, // in kB +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemComponent { + pub label: String, + pub temp: f32, + pub max: f32, + #[serde(skip_serializing_if = "Option::is_none")] + pub critical: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemProcess { + pub pid: u32, + pub name: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub exe: String, + pub cmd: Vec, + #[serde(default)] + pub start_time: f64, + pub cpu_perc: f32, + pub mem_mb: f64, + pub disk_read_kb: f64, + pub disk_write_kb: f64, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct SystemStatsRecord { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + pub server_id: String, + pub ts: f64, // unix ts milliseconds + #[serde(default)] + pub system_load: f64, + pub cpu_perc: f32, // in % + #[serde(default)] + pub cpu_freq_mhz: f64, // in MHz + pub mem_used_gb: f64, // in GB + pub mem_total_gb: f64, // in GB + pub disk: DiskUsage, + #[serde(default)] + pub cpus: Vec, + #[serde(default)] + pub networks: Vec, + #[serde(default)] + pub components: Vec, + #[serde(default)] + pub processes: Vec, + pub polling_rate: Timelength, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct HistoricalStatsQuery { + #[serde(default = "default_interval")] + pub interval: Timelength, + #[serde(default = "default_limit")] + pub limit: f64, + #[serde(default)] + pub page: f64, + #[serde(default)] + pub networks: bool, + #[serde(default)] + pub components: bool, +} + +impl Default for HistoricalStatsQuery { + fn default() -> Self { + HistoricalStatsQuery { + interval: default_interval(), + limit: default_limit(), + page: Default::default(), + networks: Default::default(), + components: Default::default(), + } + } +} + +fn default_interval() -> Timelength { + Timelength::OneHour +} + +fn default_limit() -> f64 { + 100.0 +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemInformation { + pub name: Option, + pub os: Option, + pub kernel: Option, + pub core_count: Option, + pub host_name: Option, + pub cpu_brand: String, +} + +impl TryFrom for monitor_client::entities::server::Server { + type Error = anyhow::Error; + fn try_from(value: Server) -> Result { + let server = Self { + id: value.id, + name: value.name, + description: value.description, + // permissions: value + // .permissions + // .into_iter() + // .map(|(id, p)| (id, p.into())) + // .collect(), + updated_at: unix_from_monitor_ts(&value.updated_at)?, + tags: Vec::new(), + info: (), + config: monitor_client::entities::server::ServerConfig { + address: value.address, + enabled: value.enabled, + auto_prune: value.auto_prune, + send_unreachable_alerts: true, + stats_monitoring: true, + send_cpu_alerts: true, + send_mem_alerts: true, + send_disk_alerts: true, + region: value.region.unwrap_or_default(), + cpu_warning: value.cpu_alert, + cpu_critical: value.cpu_alert, + mem_warning: value.mem_alert, + mem_critical: value.mem_alert, + disk_warning: value.disk_alert, + disk_critical: value.disk_alert, + }, + }; + Ok(server) + } +} diff --git a/bin/migrator/src/legacy/v0/traits.rs b/bin/migrator/src/legacy/v0/traits.rs new file mode 100644 index 000000000..d40874b34 --- /dev/null +++ b/bin/migrator/src/legacy/v0/traits.rs @@ -0,0 +1,112 @@ +use super::{ + Build, BuildActionState, CloneArgs, CommandActionState, Deployment, + DeploymentActionState, Group, PeripheryCommand, PermissionLevel, + PermissionsMap, Procedure, Server, ServerActionState, +}; + +pub trait Permissioned { + fn permissions_map(&self) -> &PermissionsMap; + + fn get_user_permissions(&self, user_id: &str) -> PermissionLevel { + *self.permissions_map().get(user_id).unwrap_or_default() + } +} + +impl Permissioned for Deployment { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +impl Permissioned for Build { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +impl Permissioned for Server { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +impl Permissioned for Procedure { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +impl Permissioned for Group { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +impl Permissioned for PeripheryCommand { + fn permissions_map(&self) -> &PermissionsMap { + &self.permissions + } +} + +pub trait Busy { + fn busy(&self) -> bool; +} + +impl Busy for ServerActionState { + fn busy(&self) -> bool { + self.pruning_containers + || self.pruning_images + || self.pruning_networks + } +} + +impl Busy for DeploymentActionState { + fn busy(&self) -> bool { + self.deploying + || self.pulling + || self.recloning + || self.removing + || self.starting + || self.stopping + || self.updating + || self.renaming + } +} + +impl Busy for BuildActionState { + fn busy(&self) -> bool { + self.building || self.updating + } +} + +impl Busy for CommandActionState { + fn busy(&self) -> bool { + self.running + } +} + +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.pre_build.clone(), + on_pull: None, + github_account: b.github_account.clone(), + } + } +} diff --git a/bin/migrator/src/legacy/v0/update.rs b/bin/migrator/src/legacy/v0/update.rs new file mode 100644 index 000000000..27c8b506d --- /dev/null +++ b/bin/migrator/src/legacy/v0/update.rs @@ -0,0 +1,145 @@ +use monitor_client::entities::update::ResourceTarget; +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::{ + unix_from_monitor_ts, Build, Deployment, Group, Operation, + Procedure, Server, Version, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Update { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + pub target: UpdateTarget, + pub operation: Operation, + pub logs: Vec, + pub start_ts: String, + pub end_ts: Option, + pub status: UpdateStatus, + pub success: bool, + pub operator: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +impl TryFrom for monitor_client::entities::update::Update { + type Error = anyhow::Error; + fn try_from(value: Update) -> Result { + let target: Option = value.target.into(); + let update = Self { + id: value.id, + operation: value.operation.into(), + start_ts: unix_from_monitor_ts(&value.start_ts)?, + success: value.success, + operator: value.operator, + target: target.unwrap_or_default(), + logs: value + .logs + .into_iter() + .map(|log| log.try_into()) + .collect::, + >>()?, + end_ts: value + .end_ts + .and_then(|ts| unix_from_monitor_ts(&ts).ok()), + status: value.status.into(), + version: value.version.map(|v| v.into()).unwrap_or_default(), + }; + Ok(update) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Log { + pub stage: String, + pub command: String, + pub stdout: String, + pub stderr: String, + pub success: bool, + pub start_ts: String, + pub end_ts: String, +} + +impl TryFrom for monitor_client::entities::update::Log { + type Error = anyhow::Error; + fn try_from(value: Log) -> Result { + Ok(Self { + stage: value.stage, + command: value.command, + stdout: value.stdout, + stderr: value.stderr, + success: value.success, + start_ts: unix_from_monitor_ts(&value.start_ts)?, + end_ts: unix_from_monitor_ts(&value.end_ts)?, + }) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(tag = "type", content = "id")] +pub enum UpdateTarget { + #[default] + System, + Build(String), + Deployment(String), + Server(String), + Procedure(String), + Group(String), + Command(String), +} + +impl From + for Option +{ + fn from(value: UpdateTarget) -> Self { + use monitor_client::entities::update::ResourceTarget::*; + match value { + UpdateTarget::System => Some(System("system".to_string())), + UpdateTarget::Build(id) => Some(Build(id)), + UpdateTarget::Deployment(id) => Some(Deployment(id)), + UpdateTarget::Server(id) => Some(Server(id)), + UpdateTarget::Procedure(_) => None, + UpdateTarget::Group(_) => None, + UpdateTarget::Command(id) => None, + } + } +} + +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +pub enum UpdateStatus { + Queued, + InProgress, + #[default] + Complete, +} + +impl From + for monitor_client::entities::update::UpdateStatus +{ + fn from(value: UpdateStatus) -> Self { + use monitor_client::entities::update::UpdateStatus::*; + match value { + UpdateStatus::Queued => Queued, + UpdateStatus::InProgress => InProgress, + UpdateStatus::Complete => Complete, + } + } +} diff --git a/bin/migrator/src/legacy/v0/user.rs b/bin/migrator/src/legacy/v0/user.rs new file mode 100644 index 000000000..c51d12faa --- /dev/null +++ b/bin/migrator/src/legacy/v0/user.rs @@ -0,0 +1,114 @@ +use anyhow::anyhow; +use monitor_client::entities::user::UserConfig; +use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id; +use serde::{Deserialize, Serialize}; + +use super::unix_from_monitor_ts; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct User { + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: String, + + pub username: String, + + #[serde(default)] + pub enabled: bool, + + #[serde(default)] + pub admin: bool, + + #[serde(default)] + pub create_server_permissions: bool, + + #[serde(default)] + pub create_build_permissions: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + // used with auth + #[serde(default)] + pub secrets: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub github_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub google_id: Option, + + #[serde(default, skip_serializing_if = "String::is_empty")] + pub created_at: String, + #[serde(default)] + pub updated_at: String, +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct ApiSecret { + pub name: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub hash: String, + pub created_at: String, + pub expires: Option, +} + +// impl TryFrom +// for monitor_client::entities::user::ApiSecret +// { +// type Error = anyhow::Error; +// fn try_from(value: ApiSecret) -> Result { +// let secret = Self { +// name: value.name, +// hash: value.hash, +// created_at: unix_from_monitor_ts(&value.created_at)?, +// expires: value +// .expires +// .and_then(|exp| unix_from_monitor_ts(&exp).ok()), +// }; +// Ok(secret) +// } +// } + +impl TryFrom for monitor_client::entities::user::User { + type Error = anyhow::Error; + fn try_from(value: User) -> Result { + let config = + match (value.password, value.github_id, value.google_id) { + (Some(password), _, _) => UserConfig::Local { password }, + (None, Some(github_id), _) => UserConfig::Github { + github_id, + avatar: value.avatar.unwrap_or_default(), + }, + (None, None, Some(google_id)) => UserConfig::Google { + google_id, + avatar: value.avatar.unwrap_or_default(), + }, + _ => { + return Err(anyhow!("user is not local, github, or google")) + } + }; + let user = Self { + config, + id: value.id, + username: value.username, + enabled: value.enabled, + admin: value.admin, + create_server_permissions: value.create_server_permissions, + create_build_permissions: value.create_build_permissions, + last_update_view: Default::default(), + recently_viewed: Default::default(), + updated_at: unix_from_monitor_ts(&value.updated_at)?, + }; + Ok(user) + } +} diff --git a/bin/migrator/src/main.rs b/bin/migrator/src/main.rs new file mode 100644 index 000000000..4ebf66bdf --- /dev/null +++ b/bin/migrator/src/main.rs @@ -0,0 +1,22 @@ +#![allow(unused)] +#[macro_use] +extern crate tracing; + +use crate::state::State; + +mod legacy; +mod migrate; +mod state; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logger::init(&Default::default())?; + + info!("starting migrator"); + + let state = State::load().await?; + + state.migrate_all().await?; + + Ok(()) +} diff --git a/bin/migrator/src/migrate.rs b/bin/migrator/src/migrate.rs new file mode 100644 index 000000000..84544e384 --- /dev/null +++ b/bin/migrator/src/migrate.rs @@ -0,0 +1,155 @@ +use anyhow::Context; +use mongo_indexed::Indexed; +use monitor_client::entities::{ + build::Build, + deployment::{Deployment, DeploymentConfig}, + server::Server, + update::{ResourceTarget, Update}, + user::User, +}; +use mungos::find::find_collect; + +use crate::{ + legacy::{self, v0}, + state::State, +}; + +impl State { + pub async fn migrate_all(&self) -> anyhow::Result<()> { + self.migrate_users().await?; + self.migrate_servers().await?; + self.migrate_deployments().await?; + self.migrate_builds().await?; + self.migrate_updates().await?; + Ok(()) + } + + pub async fn migrate_users(&self) -> anyhow::Result<()> { + let users = find_collect(&self.legacy.users, None, None) + .await + .context("failed to get legacy users")? + .into_iter() + .map(|s| { + let context = + format!("failed to convert user {}", s.username); + s.try_into().context(context) + }) + .collect::>>()?; + + info!("migrating {} users...", users.len()); + + self + .target + .users + .insert_many(users, None) + .await + .context("failed to insert users on target")?; + + info!("users have been migrated\n"); + + Ok(()) + } + + pub async fn migrate_servers(&self) -> anyhow::Result<()> { + let servers = find_collect(&self.legacy.servers, None, None) + .await + .context("failed to get legacy servers")? + .into_iter() + .map(|s| { + let context = format!("failed to convert server {}", s.name); + s.try_into().context(context) + }) + .collect::>>()?; + + info!("migrating {} servers...", servers.len()); + + self + .target + .servers + .insert_many(servers, None) + .await + .context("failed to insert servers on target")?; + + info!("servers have been migrated\n"); + + Ok(()) + } + + pub async fn migrate_deployments(&self) -> anyhow::Result<()> { + let deployments = + find_collect(&self.legacy.deployments, None, None) + .await + .context("failed to get legacy deployments")? + .into_iter() + .map(|s| { + let context = + format!("failed to convert deployment {}", s.name); + s.try_into().context(context) + }) + .collect::>>()?; + + info!("migrating {} deployments...", deployments.len()); + + self + .target + .deployments + .insert_many(deployments, None) + .await + .context("failed to insert deployments on target")?; + + info!("deployments have been migrated\n"); + + Ok(()) + } + + pub async fn migrate_builds(&self) -> anyhow::Result<()> { + let builds = find_collect(&self.legacy.builds, None, None) + .await + .context("failed to get legacy builds")? + .into_iter() + .map(|s| { + let context = format!("failed to convert build {}", s.name); + s.try_into().context(context) + }) + .collect::>>()?; + + info!("migrating {} builds...", builds.len()); + + self + .target + .builds + .insert_many(builds, None) + .await + .context("failed to insert builds on target")?; + + info!("builds have been migrated\n"); + + Ok(()) + } + + pub async fn migrate_updates(&self) -> anyhow::Result<()> { + let updates = find_collect(&self.legacy.updates, None, None) + .await + .context("failed to get legacy updates")? + .into_iter() + .map(|s| { + let context = + format!("failed to convert update | _id {}", s.id); + s.try_into().context(context) + }) + .collect::>>()?; + + info!("migrating {} updates...", updates.len()); + + self + .target + .updates + .insert_many(updates, None) + .await + .context("failed to insert updates on target")?; + + info!("updates have been migrated\n"); + + Ok(()) + } +} diff --git a/bin/migrator/src/state.rs b/bin/migrator/src/state.rs new file mode 100644 index 000000000..d261c9ff1 --- /dev/null +++ b/bin/migrator/src/state.rs @@ -0,0 +1,106 @@ +use anyhow::Context; +use mongo_indexed::{create_index, create_unique_index, Indexed}; +use monitor_client::entities::{ + build::Build, deployment::Deployment, server::Server, + update::Update, user::User, +}; +use mungos::{ + init::MongoBuilder, + mongodb::{Client, Collection, Database}, +}; +use serde::Deserialize; + +use crate::legacy::v0; + +#[derive(Deserialize, Debug)] +struct Env { + legacy_uri: String, + legacy_db_name: String, + target_uri: String, + target_db_name: String, +} + +pub struct State { + pub legacy: LegacyDbClient, + pub target: DbClient, +} + +impl State { + pub async fn load() -> anyhow::Result { + dotenv::dotenv().ok(); + let env = envy::from_env::()?; + let legacy_client = MongoBuilder::default() + .uri(&env.legacy_uri) + .build() + .await + .context("failed to init legacy mongo client")?; + let target_client = MongoBuilder::default() + .uri(&env.target_uri) + .build() + .await + .context("failed to init target mongo client")?; + let state = State { + legacy: LegacyDbClient::new( + &legacy_client.database(&env.legacy_db_name), + ), + target: DbClient::new( + &target_client.database(&env.target_db_name), + ) + .await?, + }; + Ok(state) + } +} + +pub struct DbClient { + pub users: Collection, + pub updates: Collection, + pub servers: Collection, + pub deployments: Collection, + pub builds: Collection, +} + +impl DbClient { + pub async fn new(db: &Database) -> anyhow::Result { + Ok(DbClient { + users: User::collection(db, true).await?, + updates: Update::collection(db, true).await?, + servers: resource_collection(db, "Server").await?, + deployments: resource_collection(db, "Deployment").await?, + builds: resource_collection(db, "Build").await?, + }) + } +} + +async fn resource_collection( + db: &Database, + collection_name: &str, +) -> anyhow::Result> { + let coll = db.collection::(collection_name); + + create_unique_index(&coll, "name").await?; + + create_index(&coll, "tags").await?; + + Ok(coll) +} + +pub struct LegacyDbClient { + pub users: Collection, + pub servers: Collection, + pub deployments: Collection, + pub builds: Collection, + pub updates: Collection, +} + +impl LegacyDbClient { + pub fn new(db: &Database) -> LegacyDbClient { + LegacyDbClient { + users: db.collection("users"), + servers: db.collection("servers"), + deployments: db.collection("deployments"), + builds: db.collection("builds"), + updates: db.collection("updates"), + } + } +} diff --git a/bin/monrun/Cargo.toml b/bin/monrun/Cargo.toml new file mode 100644 index 000000000..a0d707355 --- /dev/null +++ b/bin/monrun/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "monrun" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local client +monitor_client.workspace = true +# mogh +partial_derive2.workspace = true +# external +anyhow.workspace = true +tokio.workspace = true +serde.workspace = true +strum.workspace = true +toml.workspace = true +clap.workspace = true +futures.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true \ No newline at end of file diff --git a/docsite/docs/api/websocket.mdx b/bin/monrun/exec/exec1.toml similarity index 100% rename from docsite/docs/api/websocket.mdx rename to bin/monrun/exec/exec1.toml diff --git a/bin/monrun/readme.md b/bin/monrun/readme.md new file mode 100644 index 000000000..7b62438a4 --- /dev/null +++ b/bin/monrun/readme.md @@ -0,0 +1,9 @@ +## Examples + +```sh +monrun resources up ./resources/prod_hub.toml +``` + +```sh +monrun exec +``` \ No newline at end of file diff --git a/bin/monrun/resources/builds.toml b/bin/monrun/resources/builds.toml new file mode 100644 index 000000000..5bfb91c3c --- /dev/null +++ b/bin/monrun/resources/builds.toml @@ -0,0 +1,46 @@ +[[build]] +name = "test_logger" +tags = ["test"] +config.builder = "mogh-builder" +config.repo = "mbecker20/test_logger" +config.branch = "master" +config.github_account = "mbecker20" +config.docker_account = "mbecker2020" + +[[build]] +name = "alert_logger" +tags = ["test"] +config.builder = "mogh-builder" +config.repo = "mbecker20/monitor" +config.branch = "next" +config.build_path = "." +config.dockerfile_path = "bin/alert_logger/Dockerfile" +config.docker_account = "mbecker2020" + +[[build]] +name = "update_logger" +tags = ["test"] +config.builder = "mogh-builder" +config.repo = "mbecker20/monitor" +config.branch = "next" +config.build_path = "." +config.dockerfile_path = "bin/update_logger/Dockerfile" +config.docker_account = "mbecker2020" + +# BUILDER +[[builder]] +name = "mogh-builder" +description = "the builder for the mogh images" +config.type = "Aws" +config.params.region = "us-east-2" +config.params.instance_type = "c5.2xlarge" +config.params.volume_gb = 20 +config.params.ami_id = "ami-01c8bdba63708cde6" +config.params.subnet_id = "subnet-02ae5ad480eacc4bc" +config.params.key_pair_name = "mogh-key" +config.params.assign_public_ip = true +config.params.use_public_ip = false +config.params.port = 8000 +config.params.security_group_ids = ["sg-049d98c819f9ace58", "sg-006c0ca638af8eb44"] +config.params.github_accounts = ["mbecker20"] +config.params.docker_accounts = ["mbecker2020"] \ No newline at end of file diff --git a/bin/monrun/resources/deployments.toml b/bin/monrun/resources/deployments.toml new file mode 100644 index 000000000..7b8365753 --- /dev/null +++ b/bin/monrun/resources/deployments.toml @@ -0,0 +1,51 @@ +[[deployment]] +name = "hello-world" +tags = ["test"] +config.server = "mogh-server" +config.image.type = "Image" +config.image.params.image = "hello-world" + +[[deployment]] +name = "test_logger" +tags = ["test"] +config.server = "mogh-server" +config.image.type = "Build" +config.image.params.build = "test_logger" + +[[deployment]] +name = "alert_logger" +tags = ["test"] +config.server = "mogh-server" +config.image.type = "Build" +config.image.params.build = "alert_logger" + +[[deployment]] +name = "update_logger" +tags = ["test", "logging"] +config.server = "mogh-server" +config.image.type = "Build" +config.image.params.build = "update_logger" +[[deployment.config.environment]] +variable = "MONITOR_ADDRESS" +value = "http://localhost:9001" + +[[deployment]] +name = "vector" +tags = ["logging"] +config.server = "mogh-server" +config.image.type = "Image" +config.image.params.image = "timberio/vector" + +[[deployment]] +name = "loki" +tags = ["logging"] +config.server = "mogh-server" +config.image.type = "Image" +config.image.params.image = "grafana/loki" + +[[deployment]] +name = "grafana" +tags = ["frontend"] +config.server = "mogh-server" +config.image.type = "Image" +config.image.params.image = "grafana/grafana" \ No newline at end of file diff --git a/bin/monrun/resources/procedures.toml b/bin/monrun/resources/procedures.toml new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/bin/monrun/resources/procedures.toml @@ -0,0 +1 @@ + diff --git a/bin/monrun/resources/servers.toml b/bin/monrun/resources/servers.toml new file mode 100644 index 000000000..7070a0356 --- /dev/null +++ b/bin/monrun/resources/servers.toml @@ -0,0 +1,13 @@ +[[server]] +name = "mogh-server" +description = "main mogh server" +tags = ["mogh", "prod"] +config.address = "http://localhost:8001" +config.region = "us-east-2" + +[[server]] +name = "testo" +description = "test server 1" +tags = ["mogh", "test"] +config.address = "http://locahost:8120" +config.enabled = false \ No newline at end of file diff --git a/bin/monrun/resources/user_groups.toml b/bin/monrun/resources/user_groups.toml new file mode 100644 index 000000000..2259c8ac9 --- /dev/null +++ b/bin/monrun/resources/user_groups.toml @@ -0,0 +1,28 @@ +[[user_group]] +name = "uso_groupo" +users = ["becker.maxh", "defi moses"] + +[[user_group.permission]] +target.type = "Server" +target.id = "mogh-server" +level = "Read" + +[[user_group.permission]] +target.type = "Deployment" +target.id = "monitor-core-v1-dev" +level = "Execute" + +[[user_group.permission]] +target.type = "Deployment" +target.id = "monitor-frontend-v1-dev" +level = "Execute" + +[[user_group.permission]] +target.type = "Build" +target.id = "monitor-core-next" +level = "Execute" + +[[user_group.permission]] +target.type = "Build" +target.id = "monitor-frontend-next" +level = "Execute" \ No newline at end of file diff --git a/bin/monrun/src/execution.rs b/bin/monrun/src/execution.rs new file mode 100644 index 000000000..c33844c41 --- /dev/null +++ b/bin/monrun/src/execution.rs @@ -0,0 +1,189 @@ +use std::path::Path; + +use anyhow::{anyhow, Context}; +use futures::future::join_all; +use monitor_client::api::execute; +use serde::Deserialize; +use strum::Display; + +use crate::monitor_client; + +pub async fn run_execution(path: &Path) -> anyhow::Result<()> { + let ExecutionFile { name, stages } = crate::parse_toml_file(path)?; + + info!("EXECUTION: {name}"); + info!("path: {path:?}"); + println!("{stages:#?}"); + + crate::wait_for_enter("EXECUTE")?; + + run_stages(stages) + .await + .context("failed during a stage. terminating run.")?; + + info!("finished successfully ✅"); + + Ok(()) +} + +/// Specifies sequence of stages (build / deploy) on resources +#[derive(Debug, Clone, Deserialize)] +pub struct ExecutionFile { + pub name: String, + #[serde(rename = "stage")] + pub stages: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Stage { + pub name: String, + pub action: ExecutionType, + /// resource names + pub targets: Vec, +} + +#[derive(Debug, Clone, Copy, Deserialize, Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ExecutionType { + Build, + Deploy, + StartContainer, + StopContainer, + DestroyContainer, +} + +pub async fn run_stages(stages: Vec) -> anyhow::Result<()> { + for Stage { + name, + action, + targets, + } in stages + { + info!("running {action} stage: {name}... ⏳"); + match action { + ExecutionType::Build => { + trigger_builds_in_parallel(&targets).await?; + } + ExecutionType::Deploy => { + redeploy_deployments_in_parallel(&targets).await?; + } + ExecutionType::StartContainer => { + start_containers_in_parallel(&targets).await? + } + ExecutionType::StopContainer => { + stop_containers_in_parallel(&targets).await? + } + ExecutionType::DestroyContainer => { + destroy_containers_in_parallel(&targets).await?; + } + } + info!("finished {action} stage: {name} ✅"); + } + Ok(()) +} + +async fn redeploy_deployments_in_parallel( + deployments: &[String], +) -> anyhow::Result<()> { + let futes = deployments.iter().map(|deployment| async move { + monitor_client() + .execute(execute::Deploy { deployment: deployment.to_string(), stop_signal: None, stop_time: None }) + .await + .with_context(|| format!("failed to deploy {deployment}")) + .and_then(|update| { + if update.success { + Ok(()) + } else { + Err(anyhow!( + "failed to deploy {deployment}. operation unsuccessful, see monitor update" + )) + } + }) + }); + join_all(futes).await.into_iter().collect() +} + +async fn start_containers_in_parallel( + deployments: &[String], +) -> anyhow::Result<()> { + let futes = deployments.iter().map(|deployment| async move { + monitor_client() + .execute(execute::StartContainer { deployment: deployment.to_string() }) + .await + .with_context(|| format!("failed to start container {deployment}")) + .and_then(|update| { + if update.success { + Ok(()) + } else { + Err(anyhow!( + "failed to start container {deployment}. operation unsuccessful, see monitor update" + )) + } + }) + }); + join_all(futes).await.into_iter().collect() +} + +async fn stop_containers_in_parallel( + deployments: &[String], +) -> anyhow::Result<()> { + let futes = deployments.iter().map(|deployment| async move { + monitor_client() + .execute(execute::StopContainer { deployment: deployment.to_string(), signal: None, time: None }) + .await + .with_context(|| format!("failed to stop container {deployment}")) + .and_then(|update| { + if update.success { + Ok(()) + } else { + Err(anyhow!( + "failed to stop container {deployment}. operation unsuccessful, see monitor update" + )) + } + }) + }); + join_all(futes).await.into_iter().collect() +} + +async fn destroy_containers_in_parallel( + deployments: &[String], +) -> anyhow::Result<()> { + let futes = deployments.iter().map(|deployment| async move { + monitor_client() + .execute(execute::RemoveContainer { deployment: deployment.to_string(), signal: None, time: None }) + .await + .with_context(|| format!("failed to destroy container {deployment}")) + .and_then(|update| { + if update.success { + Ok(()) + } else { + Err(anyhow!( + "failed to destroy container {deployment}. operation unsuccessful, see monitor update" + )) + } + }) + }); + join_all(futes).await.into_iter().collect() +} + +async fn trigger_builds_in_parallel( + builds: &[String], +) -> anyhow::Result<()> { + let futes = builds.iter().map(|build| async move { + monitor_client() + .execute(execute::RunBuild { build: build.to_string() }) + .await + .with_context(|| format!("failed to build {build}")) + .and_then(|update| { + if update.success { + Ok(()) + } else { + Err(anyhow!( + "failed to build {build}. operation unsuccessful, see monitor update" + )) + } + }) + }); + join_all(futes).await.into_iter().collect() +} diff --git a/bin/monrun/src/main.rs b/bin/monrun/src/main.rs new file mode 100644 index 000000000..5ef8a8ac1 --- /dev/null +++ b/bin/monrun/src/main.rs @@ -0,0 +1,99 @@ +#[macro_use] +extern crate tracing; + +use std::{io::Read, path::PathBuf, str::FromStr, sync::OnceLock}; + +use anyhow::Context; +use clap::{Parser, Subcommand}; +use monitor_client::{api::read, MonitorClient}; +use serde::{de::DeserializeOwned, Deserialize}; + +mod execution; +mod maps; +mod sync; + +fn cli_args() -> &'static CliArgs { + static CLI_ARGS: OnceLock = OnceLock::new(); + CLI_ARGS.get_or_init(CliArgs::parse) +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct CliArgs { + #[command(subcommand)] + command: Command, + #[arg(long, default_value_t = String::from("./creds.toml"))] + creds: String, + #[arg(long, default_value_t = false)] + verbose: bool, +} + +#[derive(Debug, Clone, Subcommand)] +enum Command { + /// Runs syncs on resource files + Sync { + /// The path of the resource folder / file + /// Folder paths will recursively incorporate all the resources it finds under the folder + #[arg(default_value_t = String::from("./resources"))] + path: String, + }, + + /// Runs execution files + Exec { + /// The path of the exec file + path: PathBuf, + }, +} + +#[derive(Debug, Deserialize)] +struct CredsFile { + url: String, + key: String, + secret: String, +} + +fn monitor_client() -> &'static MonitorClient { + static MONITOR_CLIENT: OnceLock = OnceLock::new(); + MONITOR_CLIENT.get_or_init(|| { + let CredsFile { url, key, secret } = + parse_toml_file(&cli_args().creds) + .expect("failed to parse monitor credentials"); + futures::executor::block_on(MonitorClient::new(url, key, secret)) + .expect("failed to initialize monitor client") + }) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let version = + monitor_client().read(read::GetVersion {}).await?.version; + info!("monitor version: {version}"); + + match &cli_args().command { + Command::Exec { path } => execution::run_execution(path).await?, + Command::Sync { path } => { + sync::run_sync(&PathBuf::from_str(path)?).await? + } + } + + Ok(()) +} + +fn parse_toml_file( + path: impl AsRef, +) -> anyhow::Result { + let contents = std::fs::read_to_string(path) + .context("failed to read file contents")?; + toml::from_str(&contents).context("failed to parse toml contents") +} + +fn wait_for_enter(message: &str) -> anyhow::Result<()> { + println!("\nPress ENTER to {message}\n"); + let buffer = &mut [0u8]; + std::io::stdin() + .read_exact(buffer) + .context("failed to read ENTER")?; + Ok(()) +} diff --git a/bin/monrun/src/maps.rs b/bin/monrun/src/maps.rs new file mode 100644 index 000000000..866d959a5 --- /dev/null +++ b/bin/monrun/src/maps.rs @@ -0,0 +1,264 @@ +use std::{collections::HashMap, sync::OnceLock}; + +use monitor_client::{ + api::read, + entities::{ + alerter::AlerterListItem, build::BuildListItem, + builder::BuilderListItem, deployment::DeploymentListItem, + procedure::ProcedureListItem, repo::RepoListItem, + server::ServerListItem, server_template::ServerTemplateListItem, + user::User, user_group::UserGroup, + }, +}; + +use crate::monitor_client; + +pub fn name_to_build() -> &'static HashMap { + static NAME_TO_BUILD: OnceLock> = + OnceLock::new(); + NAME_TO_BUILD.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListBuilds::default()), + ) + .expect("failed to get builds from monitor") + .into_iter() + .map(|build| (build.name.clone(), build)) + .collect() + }) +} + +pub fn id_to_build() -> &'static HashMap { + static ID_TO_BUILD: OnceLock> = + OnceLock::new(); + ID_TO_BUILD.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListBuilds::default()), + ) + .expect("failed to get builds from monitor") + .into_iter() + .map(|build| (build.id.clone(), build)) + .collect() + }) +} + +pub fn name_to_deployment( +) -> &'static HashMap { + static NAME_TO_DEPLOYMENT: OnceLock< + HashMap, + > = OnceLock::new(); + NAME_TO_DEPLOYMENT.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListDeployments::default()), + ) + .expect("failed to get deployments from monitor") + .into_iter() + .map(|deployment| (deployment.name.clone(), deployment)) + .collect() + }) +} + +pub fn id_to_deployment( +) -> &'static HashMap { + static ID_TO_DEPLOYMENT: OnceLock< + HashMap, + > = OnceLock::new(); + ID_TO_DEPLOYMENT.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListDeployments::default()), + ) + .expect("failed to get deployments from monitor") + .into_iter() + .map(|deployment| (deployment.id.clone(), deployment)) + .collect() + }) +} + +pub fn name_to_server() -> &'static HashMap { + static NAME_TO_SERVER: OnceLock> = + OnceLock::new(); + NAME_TO_SERVER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListServers::default()), + ) + .expect("failed to get servers from monitor") + .into_iter() + .map(|server| (server.name.clone(), server)) + .collect() + }) +} + +pub fn id_to_server() -> &'static HashMap { + static ID_TO_SERVER: OnceLock> = + OnceLock::new(); + ID_TO_SERVER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListServers::default()), + ) + .expect("failed to get servers from monitor") + .into_iter() + .map(|server| (server.id.clone(), server)) + .collect() + }) +} + +pub fn name_to_builder() -> &'static HashMap +{ + static NAME_TO_BUILDER: OnceLock> = + OnceLock::new(); + NAME_TO_BUILDER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListBuilders::default()), + ) + .expect("failed to get builders from monitor") + .into_iter() + .map(|builder| (builder.name.clone(), builder)) + .collect() + }) +} + +pub fn id_to_builder() -> &'static HashMap { + static ID_TO_BUILDER: OnceLock> = + OnceLock::new(); + ID_TO_BUILDER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListBuilders::default()), + ) + .expect("failed to get builders from monitor") + .into_iter() + .map(|builder| (builder.id.clone(), builder)) + .collect() + }) +} + +pub fn name_to_alerter() -> &'static HashMap +{ + static NAME_TO_ALERTER: OnceLock> = + OnceLock::new(); + NAME_TO_ALERTER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListAlerters::default()), + ) + .expect("failed to get alerters from monitor") + .into_iter() + .map(|alerter| (alerter.name.clone(), alerter)) + .collect() + }) +} + +pub fn id_to_alerter() -> &'static HashMap { + static ID_TO_ALERTER: OnceLock> = + OnceLock::new(); + ID_TO_ALERTER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListAlerters::default()), + ) + .expect("failed to get alerters from monitor") + .into_iter() + .map(|alerter| (alerter.id.clone(), alerter)) + .collect() + }) +} + +pub fn name_to_repo() -> &'static HashMap { + static NAME_TO_ALERTER: OnceLock> = + OnceLock::new(); + NAME_TO_ALERTER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListRepos::default()), + ) + .expect("failed to get repos from monitor") + .into_iter() + .map(|repo| (repo.name.clone(), repo)) + .collect() + }) +} + +pub fn id_to_repo() -> &'static HashMap { + static ID_TO_ALERTER: OnceLock> = + OnceLock::new(); + ID_TO_ALERTER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListRepos::default()), + ) + .expect("failed to get repos from monitor") + .into_iter() + .map(|repo| (repo.id.clone(), repo)) + .collect() + }) +} + +pub fn name_to_procedure( +) -> &'static HashMap { + static NAME_TO_PROCEDURE: OnceLock< + HashMap, + > = OnceLock::new(); + NAME_TO_PROCEDURE.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListProcedures::default()), + ) + .expect("failed to get procedures from monitor") + .into_iter() + .map(|procedure| (procedure.name.clone(), procedure)) + .collect() + }) +} + +pub fn id_to_procedure() -> &'static HashMap +{ + static ID_TO_PROCEDURE: OnceLock< + HashMap, + > = OnceLock::new(); + ID_TO_PROCEDURE.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListProcedures::default()), + ) + .expect("failed to get procedures from monitor") + .into_iter() + .map(|procedure| (procedure.id.clone(), procedure)) + .collect() + }) +} + +pub fn id_to_server_template( +) -> &'static HashMap { + static ID_TO_SERVER_TEMPLATE: OnceLock< + HashMap, + > = OnceLock::new(); + ID_TO_SERVER_TEMPLATE.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListServerTemplates::default()), + ) + .expect("failed to get procedures from monitor") + .into_iter() + .map(|procedure| (procedure.id.clone(), procedure)) + .collect() + }) +} + +pub fn name_to_user_group() -> &'static HashMap { + static NAME_TO_USER_GROUP: OnceLock> = + OnceLock::new(); + NAME_TO_USER_GROUP.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListUserGroups::default()), + ) + .expect("failed to get procedures from monitor") + .into_iter() + .map(|user_group| (user_group.name.clone(), user_group)) + .collect() + }) +} + +pub fn id_to_user() -> &'static HashMap { + static ID_TO_USER: OnceLock> = + OnceLock::new(); + ID_TO_USER.get_or_init(|| { + futures::executor::block_on( + monitor_client().read(read::ListUsers::default()), + ) + .expect("failed to get procedures from monitor") + .into_iter() + .map(|user| (user.id.clone(), user)) + .collect() + }) +} diff --git a/bin/monrun/src/sync/mod.rs b/bin/monrun/src/sync/mod.rs new file mode 100644 index 000000000..bee3ec805 --- /dev/null +++ b/bin/monrun/src/sync/mod.rs @@ -0,0 +1,62 @@ +use std::path::Path; + +use monitor_client::entities::{ + alerter::Alerter, build::Build, builder::Builder, + deployment::Deployment, procedure::Procedure, repo::Repo, + server::Server, +}; + +use crate::{sync::resources::ResourceSync, wait_for_enter}; + +mod resource_file; +mod resources; +mod user_group; + +pub async fn run_sync(path: &Path) -> anyhow::Result<()> { + info!("path: {path:?}"); + + let resources = resource_file::read_resources(path)?; + + println!("{resources:#?}"); + + let (server_creates, server_updates) = + Server::get_updates(resources.servers).await?; + let (deployment_creates, deployment_updates) = + Deployment::get_updates(resources.deployments).await?; + let (build_creates, build_updates) = + Build::get_updates(resources.builds).await?; + let (builder_creates, builder_updates) = + Builder::get_updates(resources.builders).await?; + let (alerter_creates, alerter_updates) = + Alerter::get_updates(resources.alerters).await?; + let (repo_creates, repo_updates) = + Repo::get_updates(resources.repos).await?; + let (procedure_creates, procedure_updates) = + Procedure::get_updates(resources.procedures).await?; + let (user_group_creates, user_group_updates) = + user_group::get_updates(resources.user_groups).await?; + + wait_for_enter("CONTINUE")?; + + // No deps + Server::run_updates(server_creates, server_updates).await; + Alerter::run_updates(alerter_creates, alerter_updates).await; + + // Dependant on server + Builder::run_updates(builder_creates, builder_updates).await; + Repo::run_updates(repo_creates, repo_updates).await; + + // Dependant on builder + Build::run_updates(build_creates, build_updates).await; + + // Dependant on server / builder + Deployment::run_updates(deployment_creates, deployment_updates) + .await; + + // Dependant on everything + Procedure::run_updates(procedure_creates, procedure_updates).await; + user_group::run_updates(user_group_creates, user_group_updates) + .await; + + Ok(()) +} diff --git a/bin/monrun/src/sync/resource_file.rs b/bin/monrun/src/sync/resource_file.rs new file mode 100644 index 000000000..8ff06daef --- /dev/null +++ b/bin/monrun/src/sync/resource_file.rs @@ -0,0 +1,57 @@ +use std::{fs, path::Path}; + +use anyhow::{anyhow, Context}; +use monitor_client::entities::toml::ResourcesToml; + +pub fn read_resources(path: &Path) -> anyhow::Result { + let mut res = ResourcesToml::default(); + read_resources_recursive(path, &mut res)?; + Ok(res) +} + +fn read_resources_recursive( + path: &Path, + resources: &mut ResourcesToml, +) -> anyhow::Result<()> { + let res = + fs::metadata(path).context("failed to get path metadata")?; + if res.is_file() { + if !path + .extension() + .map(|ext| ext == "toml") + .unwrap_or_default() + { + return Ok(()); + } + let more = match crate::parse_toml_file::(path) { + Ok(res) => res, + Err(e) => { + warn!("failed to parse {:?}. skipping file | {e:#}", path); + return Ok(()); + } + }; + info!("adding resources from {path:?}"); + resources.servers.extend(more.servers); + resources.builds.extend(more.builds); + resources.deployments.extend(more.deployments); + resources.builders.extend(more.builders); + resources.repos.extend(more.repos); + resources.alerters.extend(more.alerters); + resources.procedures.extend(more.procedures); + resources.user_groups.extend(more.user_groups); + Ok(()) + } else if res.is_dir() { + let directory = fs::read_dir(path) + .context("failed to read directory contents")?; + for entry in directory.into_iter().flatten() { + if let Err(e) = + read_resources_recursive(&entry.path(), resources) + { + warn!("failed to read additional resources at path | {e:#}"); + } + } + Ok(()) + } else { + Err(anyhow!("resources path is neither file nor directory")) + } +} diff --git a/bin/monrun/src/sync/resources/alerter.rs b/bin/monrun/src/sync/resources/alerter.rs new file mode 100644 index 000000000..8cf2d425c --- /dev/null +++ b/bin/monrun/src/sync/resources/alerter.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + read::GetAlerter, + write::{CreateAlerter, UpdateAlerter}, + }, + entities::{ + alerter::{ + Alerter, AlerterConfig, AlerterInfo, AlerterListItemInfo, + PartialAlerterConfig, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{maps::name_to_alerter, monitor_client}; + +use super::ResourceSync; + +impl ResourceSync for Alerter { + type PartialConfig = PartialAlerterConfig; + type FullConfig = AlerterConfig; + type FullInfo = AlerterInfo; + type ListItemInfo = AlerterListItemInfo; + + fn display() -> &'static str { + "alerter" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Alerter(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_alerter() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateAlerter { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateAlerter { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetAlerter { alerter: id }).await + } + + async fn minimize_update( + original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/build.rs b/bin/monrun/src/sync/resources/build.rs new file mode 100644 index 000000000..eaa82fca9 --- /dev/null +++ b/bin/monrun/src/sync/resources/build.rs @@ -0,0 +1,90 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + read::GetBuild, + write::{CreateBuild, UpdateBuild}, + }, + entities::{ + build::{ + Build, BuildConfig, BuildInfo, BuildListItemInfo, + PartialBuildConfig, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{ + maps::{id_to_builder, name_to_build}, + monitor_client, +}; + +use super::ResourceSync; + +impl ResourceSync for Build { + type PartialConfig = PartialBuildConfig; + type FullConfig = BuildConfig; + type FullInfo = BuildInfo; + type ListItemInfo = BuildListItemInfo; + + fn display() -> &'static str { + "build" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Build(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_build() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateBuild { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateBuild { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetBuild { build: id }).await + } + + async fn minimize_update( + mut original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + // need to replace the builder id with name + original.builder_id = id_to_builder() + .get(&original.builder_id) + .map(|b| b.name.clone()) + .unwrap_or_default(); + + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/builder.rs b/bin/monrun/src/sync/resources/builder.rs new file mode 100644 index 000000000..7d9bc4fa6 --- /dev/null +++ b/bin/monrun/src/sync/resources/builder.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + read::GetBuilder, + write::{CreateBuilder, UpdateBuilder}, + }, + entities::{ + builder::{ + Builder, BuilderConfig, BuilderListItemInfo, + PartialBuilderConfig, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{ + maps::{id_to_server, name_to_builder}, + monitor_client, +}; + +use super::ResourceSync; + +impl ResourceSync for Builder { + type PartialConfig = PartialBuilderConfig; + type FullConfig = BuilderConfig; + type FullInfo = (); + type ListItemInfo = BuilderListItemInfo; + + fn display() -> &'static str { + "builder" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Builder(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_builder() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateBuilder { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateBuilder { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetBuilder { builder: id }).await + } + + async fn minimize_update( + mut original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + // need to replace server builder id with name + if let BuilderConfig::Server(config) = &mut original { + config.server_id = id_to_server() + .get(&config.server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } + + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/deployment.rs b/bin/monrun/src/sync/resources/deployment.rs new file mode 100644 index 000000000..cfb464c8e --- /dev/null +++ b/bin/monrun/src/sync/resources/deployment.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{read::GetDeployment, write}, + entities::{ + deployment::{ + Deployment, DeploymentConfig, DeploymentImage, + DeploymentListItemInfo, PartialDeploymentConfig, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{ + maps::{id_to_build, id_to_server, name_to_deployment}, + monitor_client, +}; + +use super::ResourceSync; + +impl ResourceSync for Deployment { + type PartialConfig = PartialDeploymentConfig; + type FullConfig = DeploymentConfig; + type FullInfo = (); + type ListItemInfo = DeploymentListItemInfo; + + fn display() -> &'static str { + "deployment" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Deployment(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_deployment() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(write::CreateDeployment { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(write::UpdateDeployment { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client() + .read(GetDeployment { deployment: id }) + .await + } + + async fn minimize_update( + mut original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + // need to replace the server id with name + original.server_id = id_to_server() + .get(&original.server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + + // need to replace the build id with name + if let DeploymentImage::Build { build_id, version } = + &original.image + { + original.image = DeploymentImage::Build { + build_id: id_to_build() + .get(build_id) + .map(|b| b.name.clone()) + .unwrap_or_default(), + version: version.clone(), + }; + } + + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/mod.rs b/bin/monrun/src/sync/resources/mod.rs new file mode 100644 index 000000000..a5ba5c476 --- /dev/null +++ b/bin/monrun/src/sync/resources/mod.rs @@ -0,0 +1,212 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::write::{UpdateDescription, UpdateTagsOnResource}, + entities::{ + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::MaybeNone; + +use crate::{cli_args, monitor_client}; + +pub mod alerter; +pub mod build; +pub mod builder; +pub mod deployment; +pub mod procedure; +pub mod repo; +pub mod server; + +type ToUpdate = Vec<(String, ResourceToml)>; +type ToCreate = Vec>; +type UpdatesResult = (ToCreate, ToUpdate); + +pub trait ResourceSync { + type PartialConfig: std::fmt::Debug + + Clone + + Send + + MaybeNone + + 'static; + type FullConfig: Clone + Send + 'static; + type FullInfo: Default; + type ListItemInfo: 'static; + + fn display() -> &'static str; + + fn resource_target(id: String) -> ResourceTarget; + + fn name_to_resource( + ) -> &'static HashMap>; + + /// Creates the resource and returns created id. + async fn create( + resource: ResourceToml, + ) -> anyhow::Result; + + /// Updates the resource at id with the partial config. + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()>; + + async fn get( + id: String, + ) -> anyhow::Result>; + + /// Diffs the declared toml (partial) against the full existing config. + /// Removes all fields from toml (partial) that haven't changed. + async fn minimize_update( + original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result; + + async fn get_updates( + resources: Vec>, + ) -> anyhow::Result> { + let map = Self::name_to_resource(); + + let mut to_create = ToCreate::::new(); + let mut to_update = ToUpdate::::new(); + + for mut resource in resources { + match map.get(&resource.name).map(|s| s.id.clone()) { + Some(id) => { + // Get the full original config for the resource. + let original = Self::get(id.clone()).await?.config; + + // Minimizes updates through diffing. + resource.config = + Self::minimize_update(original, resource.config).await?; + + // Only try to update if there are any fields to update. + if !resource.config.is_none() { + to_update.push((id, resource)); + } + } + None => { + to_create.push(resource); + } + } + } + + let verbose = cli_args().verbose; + + if !to_create.is_empty() { + if verbose { + println!("\n{} TO CREATE:\n{to_create:#?}", Self::display(),); + } else { + println!( + "\n{} TO CREATE: {:#?}", + Self::display(), + to_create + .iter() + .map(|item| item.name.as_str()) + .collect::>() + ); + } + } + + if !to_update.is_empty() { + if verbose { + println!( + "\n{} TO UPDATE:\n{:#?}", + Self::display(), + to_update.iter().map(|(_, r)| r).collect::>() + ); + } else { + println!( + "\n{} TO UPDATE: {}", + Self::display(), + to_update + .iter() + .map(|(_, item)| item.name.as_str()) + .collect::>() + .join(", ") + ); + } + } + + Ok((to_create, to_update)) + } + + async fn run_updates( + to_create: ToCreate, + to_update: ToUpdate, + ) { + let log_after = !to_update.is_empty() || !to_create.is_empty(); + + for resource in to_create { + let name = resource.name.clone(); + let tags = resource.tags.clone(); + let description = resource.description.clone(); + let id = match Self::create(resource).await { + Ok(id) => id, + Err(e) => { + warn!( + "failed to create {} {name} | {e:#}", + Self::display(), + ); + continue; + } + }; + Self::update_tags(id.clone(), &name, tags).await; + Self::update_description(id, description).await; + info!("{} {name} created", Self::display()); + } + + for (id, resource) in to_update { + // Update resource + let name = resource.name.clone(); + let tags = resource.tags.clone(); + let description = resource.description.clone(); + if let Err(e) = Self::update(id.clone(), resource).await { + warn!("failed to update {} {name} | {e:#}", Self::display()); + } + Self::update_tags(id.clone(), &name, tags).await; + Self::update_description(id, description).await; + info!("{} {name} updated", Self::display()); + } + + if log_after { + info!( + "============ {}s synced ✅ ============", + Self::display() + ); + } + } + + async fn update_tags( + resource_id: String, + resource_name: &str, + tags: Vec, + ) { + // Update tags + if let Err(e) = monitor_client() + .write(UpdateTagsOnResource { + target: Self::resource_target(resource_id), + tags, + }) + .await + { + warn!( + "failed to update tags on {} {resource_name} | {e:#}", + Self::display(), + ); + } + } + + async fn update_description(id: String, description: String) { + if let Err(e) = monitor_client() + .write(UpdateDescription { + target: Self::resource_target(id.clone()), + description, + }) + .await + { + warn!("failed to update resource {id} description | {e:#}"); + } + } +} diff --git a/bin/monrun/src/sync/resources/procedure.rs b/bin/monrun/src/sync/resources/procedure.rs new file mode 100644 index 000000000..bf29676ec --- /dev/null +++ b/bin/monrun/src/sync/resources/procedure.rs @@ -0,0 +1,234 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + execute::Execution, + read::GetProcedure, + write::{CreateProcedure, UpdateProcedure}, + }, + entities::{ + procedure::{ + PartialProcedureConfig, Procedure, ProcedureConfig, + ProcedureListItemInfo, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{ + maps::{ + id_to_build, id_to_deployment, id_to_procedure, id_to_repo, + id_to_server, name_to_procedure, + }, + monitor_client, +}; + +use super::{ResourceSync, ToCreate, ToUpdate}; + +impl ResourceSync for Procedure { + type PartialConfig = PartialProcedureConfig; + type FullConfig = ProcedureConfig; + type FullInfo = (); + type ListItemInfo = ProcedureListItemInfo; + + fn display() -> &'static str { + "procedure" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Procedure(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_procedure() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateProcedure { + name: resource.name, + config: resource.config, + }) + .await + .map(|p| p.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateProcedure { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn run_updates( + mut to_create: ToCreate, + mut to_update: ToUpdate, + ) { + if to_update.is_empty() && to_create.is_empty() { + return; + } + + for i in 0..10 { + let mut to_pull = Vec::new(); + for (id, resource) in &to_update { + // Update resource + let name = resource.name.clone(); + let tags = resource.tags.clone(); + let description = resource.description.clone(); + if let Err(e) = + Self::update(id.clone(), resource.clone()).await + { + if i == 9 { + warn!( + "failed to update {} {name} | {e:#}", + Self::display() + ); + } + } + Self::update_tags(id.clone(), &name, tags).await; + Self::update_description(id.clone(), description).await; + info!("{} {name} updated", Self::display()); + // have to clone out so to_update is mutable + to_pull.push(id.clone()); + } + to_update.retain(|(id, _)| !to_pull.contains(id)); + + let mut to_pull = Vec::new(); + for resource in &to_create { + let name = resource.name.clone(); + let tags = resource.tags.clone(); + let description = resource.description.clone(); + let id = match Self::create(resource.clone()).await { + Ok(id) => id, + Err(e) => { + if i == 9 { + warn!( + "failed to create {} {name} | {e:#}", + Self::display(), + ); + } + continue; + } + }; + Self::update_tags(id.clone(), &name, tags).await; + Self::update_description(id, description).await; + info!("{} {name} created", Self::display()); + to_pull.push(name); + } + to_create.retain(|resource| !to_pull.contains(&resource.name)); + + if to_update.is_empty() && to_create.is_empty() { + info!( + "============ {}s synced ✅ ============", + Self::display() + ); + return; + } + } + warn!("procedure sync loop exited after max iterations"); + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetProcedure { procedure: id }).await + } + + async fn minimize_update( + mut original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + for execution in &mut original.executions { + match &mut execution.execution { + Execution::None(_) => {} + Execution::RunProcedure(config) => { + config.procedure = id_to_procedure() + .get(&config.procedure) + .map(|p| p.name.clone()) + .unwrap_or_default(); + } + Execution::RunBuild(config) => { + config.build = id_to_build() + .get(&config.build) + .map(|b| b.name.clone()) + .unwrap_or_default(); + } + Execution::Deploy(config) => { + config.deployment = id_to_deployment() + .get(&config.deployment) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::StartContainer(config) => { + config.deployment = id_to_deployment() + .get(&config.deployment) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::StopContainer(config) => { + config.deployment = id_to_deployment() + .get(&config.deployment) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::RemoveContainer(config) => { + config.deployment = id_to_deployment() + .get(&config.deployment) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::CloneRepo(config) => { + config.repo = id_to_repo() + .get(&config.repo) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::PullRepo(config) => { + config.repo = id_to_repo() + .get(&config.repo) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::StopAllContainers(config) => { + config.server = id_to_server() + .get(&config.server) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::PruneDockerNetworks(config) => { + config.server = id_to_server() + .get(&config.server) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::PruneDockerImages(config) => { + config.server = id_to_server() + .get(&config.server) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + Execution::PruneDockerContainers(config) => { + config.server = id_to_server() + .get(&config.server) + .map(|d| d.name.clone()) + .unwrap_or_default(); + } + } + } + + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/repo.rs b/bin/monrun/src/sync/resources/repo.rs new file mode 100644 index 000000000..193288471 --- /dev/null +++ b/bin/monrun/src/sync/resources/repo.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + read::GetRepo, + write::{CreateRepo, UpdateRepo}, + }, + entities::{ + repo::{ + PartialRepoConfig, Repo, RepoConfig, RepoInfo, RepoListItemInfo, + }, + resource::{Resource, ResourceListItem}, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{ + maps::{id_to_server, name_to_repo}, + monitor_client, +}; + +use super::ResourceSync; + +impl ResourceSync for Repo { + type PartialConfig = PartialRepoConfig; + type FullConfig = RepoConfig; + type FullInfo = RepoInfo; + type ListItemInfo = RepoListItemInfo; + + fn display() -> &'static str { + "repo" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Repo(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_repo() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateRepo { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateRepo { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetRepo { repo: id }).await + } + + async fn minimize_update( + mut original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + // Need to replace server id with name + original.server_id = id_to_server() + .get(&original.server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/resources/server.rs b/bin/monrun/src/sync/resources/server.rs new file mode 100644 index 000000000..e30280469 --- /dev/null +++ b/bin/monrun/src/sync/resources/server.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; + +use monitor_client::{ + api::{ + read::GetServer, + write::{CreateServer, UpdateServer}, + }, + entities::{ + resource::{Resource, ResourceListItem}, + server::{ + PartialServerConfig, Server, ServerConfig, ServerListItemInfo, + }, + toml::ResourceToml, + update::ResourceTarget, + }, +}; +use partial_derive2::PartialDiff; + +use crate::{maps::name_to_server, monitor_client}; + +use super::ResourceSync; + +impl ResourceSync for Server { + type ListItemInfo = ServerListItemInfo; + type FullConfig = ServerConfig; + type FullInfo = (); + type PartialConfig = PartialServerConfig; + + fn display() -> &'static str { + "server" + } + + fn resource_target(id: String) -> ResourceTarget { + ResourceTarget::Server(id) + } + + fn name_to_resource( + ) -> &'static HashMap> + { + name_to_server() + } + + async fn create( + resource: ResourceToml, + ) -> anyhow::Result { + monitor_client() + .write(CreateServer { + name: resource.name, + config: resource.config, + }) + .await + .map(|res| res.id) + } + + async fn update( + id: String, + resource: ResourceToml, + ) -> anyhow::Result<()> { + monitor_client() + .write(UpdateServer { + id, + config: resource.config, + }) + .await?; + Ok(()) + } + + async fn get( + id: String, + ) -> anyhow::Result> { + monitor_client().read(GetServer { server: id }).await + } + + async fn minimize_update( + original: Self::FullConfig, + update: Self::PartialConfig, + ) -> anyhow::Result { + Ok(original.partial_diff(update)) + } +} diff --git a/bin/monrun/src/sync/user_group.rs b/bin/monrun/src/sync/user_group.rs new file mode 100644 index 000000000..5cb2eaf1c --- /dev/null +++ b/bin/monrun/src/sync/user_group.rs @@ -0,0 +1,242 @@ +use std::cmp::Ordering; + +use anyhow::Context; +use monitor_client::{ + api::{ + read::ListUserTargetPermissions, + write::{ + CreateUserGroup, SetUsersInUserGroup, UpdatePermissionOnTarget, + }, + }, + entities::{ + permission::UserTarget, + toml::{PermissionToml, UserGroupToml}, + update::ResourceTarget, + }, +}; + +use crate::{ + maps::{ + id_to_alerter, id_to_build, id_to_builder, id_to_deployment, + id_to_procedure, id_to_repo, id_to_server, id_to_server_template, + id_to_user, name_to_user_group, + }, + monitor_client, +}; + +pub async fn get_updates( + user_groups: Vec, +) -> anyhow::Result<(Vec, Vec)> { + let map = name_to_user_group(); + + let mut to_create = Vec::::new(); + let mut to_update = Vec::::new(); + + for mut user_group in user_groups { + match map.get(&user_group.name).cloned() { + Some(original) => { + // replace the user ids with usernames + let mut users = original + .users + .into_iter() + .filter_map(|user_id| { + id_to_user().get(&user_id).map(|u| u.username.clone()) + }) + .collect::>(); + + let mut permissions = monitor_client() + .read(ListUserTargetPermissions { + user_target: UserTarget::UserGroup(original.id), + }) + .await + .context("failed to query for UserGroup permissions")? + .into_iter() + .map(|mut p| { + // replace the ids with names + match &mut p.resource_target { + ResourceTarget::System(_) => {} + ResourceTarget::Build(id) => { + *id = id_to_build() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Builder(id) => { + *id = id_to_builder() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Deployment(id) => { + *id = id_to_deployment() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Server(id) => { + *id = id_to_server() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Repo(id) => { + *id = id_to_repo() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Alerter(id) => { + *id = id_to_alerter() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Procedure(id) => { + *id = id_to_procedure() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::ServerTemplate(id) => { + *id = id_to_server_template() + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + } + PermissionToml { + target: p.resource_target, + level: p.level, + } + }) + .collect::>(); + + users.sort(); + user_group.users.sort(); + + user_group.permissions.sort_by(sort_permissions); + permissions.sort_by(sort_permissions); + + // only push update after failed diff + if user_group.users != users + || user_group.permissions != permissions + { + // no update from users + to_update.push(user_group); + } + } + None => to_create.push(user_group), + } + } + + if !to_create.is_empty() { + println!( + "\nUSER GROUPS TO CREATE: {}", + to_create + .iter() + .map(|item| item.name.as_str()) + .collect::>() + .join(", ") + ); + } + + if !to_update.is_empty() { + println!( + "\nUSER GROUPS TO UPDATE: {}", + to_update + .iter() + .map(|item| item.name.as_str()) + .collect::>() + .join(", ") + ); + } + + Ok((to_create, to_update)) +} + +/// order permissions in deterministic way +fn sort_permissions( + a: &PermissionToml, + b: &PermissionToml, +) -> Ordering { + let (a_t, a_id) = a.target.extract_variant_id(); + let (b_t, b_id) = b.target.extract_variant_id(); + match (a_t.cmp(&b_t), a_id.cmp(b_id)) { + (Ordering::Greater, _) => Ordering::Greater, + (Ordering::Less, _) => Ordering::Less, + (_, Ordering::Greater) => Ordering::Greater, + (_, Ordering::Less) => Ordering::Less, + _ => Ordering::Equal, + } +} + +pub async fn run_updates( + to_create: Vec, + to_update: Vec, +) { + let log_after = !to_update.is_empty() || !to_create.is_empty(); + + // Create the non-existant user groups + for user_group in to_create { + // Create the user group + if let Err(e) = monitor_client() + .write(CreateUserGroup { + name: user_group.name.clone(), + }) + .await + { + warn!( + "failed to create user group {} | {e:#}", + user_group.name + ); + continue; + }; + + set_users(user_group.name.clone(), user_group.users).await; + update_permissions(user_group.name, user_group.permissions).await; + } + + // Update the existing user groups + for user_group in to_update { + set_users(user_group.name.clone(), user_group.users).await; + update_permissions(user_group.name, user_group.permissions).await; + } + + if log_after { + info!("============ user groups synced ✅ ============"); + } +} + +async fn set_users(user_group: String, users: Vec) { + if !users.is_empty() { + if let Err(e) = monitor_client() + .write(SetUsersInUserGroup { + user_group: user_group.clone(), + users, + }) + .await + { + warn!("failed to set users in group {user_group} | {e:#}"); + } + } +} + +async fn update_permissions( + user_group: String, + permissions: Vec, +) { + for PermissionToml { target, level } in permissions { + if let Err(e) = monitor_client() + .write(UpdatePermissionOnTarget { + user_target: UserTarget::UserGroup(user_group.clone()), + resource_target: target.clone(), + permission: level, + }) + .await + { + warn!( + "failed to set permssion in group {user_group} | target: {target:?} | {e:#}", + ); + } + } +} diff --git a/bin/periphery/Cargo.toml b/bin/periphery/Cargo.toml new file mode 100644 index 000000000..e7f7c74fd --- /dev/null +++ b/bin/periphery/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "monitor_periphery" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "periphery" +path = "src/main.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +periphery_client.workspace = true +logger.workspace = true +# mogh +async_timing_util.workspace = true +merge_config_files.workspace = true +parse_csl.workspace = true +termination_signal.workspace = true +run_command.workspace = true +svi.workspace = true +resolver_api.workspace = true +serror.workspace = true +# external +tokio.workspace = true +axum.workspace = true +axum-extra.workspace = true +dotenv.workspace = true +envy.workspace = true +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +bollard.workspace = true +clap.workspace = true +tracing.workspace = true +uuid.workspace = true +sysinfo.workspace = true +async-trait.workspace = true diff --git a/bin/periphery/src/api/build.rs b/bin/periphery/src/api/build.rs new file mode 100644 index 000000000..87a4030f9 --- /dev/null +++ b/bin/periphery/src/api/build.rs @@ -0,0 +1,56 @@ +use async_trait::async_trait; +use monitor_client::entities::{ + server::docker_image::ImageSummary, update::Log, +}; +use periphery_client::api::build::{ + Build, GetImageList, PruneImages, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::docker::{self, client::docker_client}, + State, +}; + +#[async_trait] +impl Resolve for State { + #[instrument(name = "Build", skip(self))] + async fn resolve( + &self, + Build { + build, + docker_token, + }: Build, + _: (), + ) -> anyhow::Result> { + docker::build::build(&build, docker_token).await + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "GetImageList", level = "debug", skip(self))] + async fn resolve( + &self, + _: GetImageList, + _: (), + ) -> anyhow::Result> { + docker_client().list_images().await + } +} + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneImages", skip(self))] + async fn resolve( + &self, + _: PruneImages, + _: (), + ) -> anyhow::Result { + Ok(docker::build::prune_images().await) + } +} diff --git a/bin/periphery/src/api/container.rs b/bin/periphery/src/api/container.rs new file mode 100644 index 000000000..1803a794b --- /dev/null +++ b/bin/periphery/src/api/container.rs @@ -0,0 +1,225 @@ +use anyhow::anyhow; +use monitor_client::entities::{ + deployment::{ContainerSummary, DockerContainerStats}, + update::Log, +}; +use periphery_client::api::container::*; +use resolver_api::Resolve; + +use crate::{ + helpers::docker::{self, client::docker_client}, + State, +}; + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument( + name = "GetContainerList", + level = "debug", + skip(self) + )] + async fn resolve( + &self, + _: GetContainerList, + _: (), + ) -> anyhow::Result> { + docker_client().list_containers().await + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "GetContainerLog", level = "debug", skip(self))] + async fn resolve( + &self, + req: GetContainerLog, + _: (), + ) -> anyhow::Result { + Ok(docker::container::container_log(&req.name, req.tail).await) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument( + name = "GetContainerLogSearch", + level = "debug", + skip(self) + )] + async fn resolve( + &self, + req: GetContainerLogSearch, + _: (), + ) -> anyhow::Result { + Ok( + docker::container::container_log_search( + &req.name, + &req.terms, + req.combinator, + ) + .await, + ) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument( + name = "GetContainerStats", + level = "debug", + skip(self) + )] + async fn resolve( + &self, + req: GetContainerStats, + _: (), + ) -> anyhow::Result { + let error = anyhow!("no stats matching {}", req.name); + let mut stats = + docker::container::container_stats(Some(req.name)).await?; + let stats = stats.pop().ok_or(error)?; + Ok(stats) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument( + name = "GetContainerStatsList", + level = "debug", + skip(self) + )] + async fn resolve( + &self, + _: GetContainerStatsList, + _: (), + ) -> anyhow::Result> { + docker::container::container_stats(None).await + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "StartContainer", skip(self))] + async fn resolve( + &self, + req: StartContainer, + _: (), + ) -> anyhow::Result { + Ok(docker::container::start_container(&req.name).await) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "StopContainer", skip(self))] + async fn resolve( + &self, + req: StopContainer, + _: (), + ) -> anyhow::Result { + Ok( + docker::container::stop_container( + &req.name, req.signal, req.time, + ) + .await, + ) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "RemoveContainer", skip(self))] + async fn resolve( + &self, + req: RemoveContainer, + _: (), + ) -> anyhow::Result { + Ok( + docker::container::stop_and_remove_container( + &req.name, req.signal, req.time, + ) + .await, + ) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "RenameContainer", skip(self))] + async fn resolve( + &self, + req: RenameContainer, + _: (), + ) -> anyhow::Result { + Ok( + docker::container::rename_container( + &req.curr_name, + &req.new_name, + ) + .await, + ) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "PruneContainers", skip(self))] + async fn resolve( + &self, + _: PruneContainers, + _: (), + ) -> anyhow::Result { + Ok(docker::container::prune_containers().await) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "Deploy", skip(self))] + async fn resolve( + &self, + Deploy { + deployment, + docker_token, + stop_signal, + stop_time, + }: Deploy, + _: (), + ) -> anyhow::Result { + let res = docker::container::deploy( + &deployment, + stop_signal + .unwrap_or(deployment.config.termination_signal) + .into(), + stop_time + .unwrap_or(deployment.config.termination_timeout) + .into(), + docker_token, + ) + .await; + Ok(res) + } +} diff --git a/bin/periphery/src/api/git.rs b/bin/periphery/src/api/git.rs new file mode 100644 index 000000000..9336e8a30 --- /dev/null +++ b/bin/periphery/src/api/git.rs @@ -0,0 +1,65 @@ +use monitor_client::entities::{to_monitor_name, update::Log}; +use periphery_client::api::git::{CloneRepo, DeleteRepo, PullRepo}; +use resolver_api::Resolve; + +use crate::{config::periphery_config, helpers::git, State}; + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "CloneRepo", skip(self))] + async fn resolve( + &self, + CloneRepo { args, github_token }: CloneRepo, + _: (), + ) -> anyhow::Result> { + git::clone(args, github_token).await + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "PullRepo", skip(self))] + async fn resolve( + &self, + PullRepo { + name, + branch, + on_pull, + }: PullRepo, + _: (), + ) -> anyhow::Result> { + let name = to_monitor_name(&name); + Ok( + git::pull( + &periphery_config().repo_dir.join(name), + &branch, + &on_pull, + ) + .await, + ) + } +} + +// + +#[async_trait::async_trait] +impl Resolve for State { + #[instrument(name = "DeleteRepo", skip(self))] + async fn resolve( + &self, + DeleteRepo { name }: DeleteRepo, + _: (), + ) -> anyhow::Result { + let name = to_monitor_name(&name); + let deleted = std::fs::remove_dir_all( + periphery_config().repo_dir.join(&name), + ); + let msg = match deleted { + Ok(_) => format!("deleted repo {name}"), + Err(_) => format!("no repo at {name} to delete"), + }; + Ok(Log::simple("delete repo", msg)) + } +} diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs new file mode 100644 index 000000000..7e3be78dc --- /dev/null +++ b/bin/periphery/src/api/mod.rs @@ -0,0 +1,165 @@ +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::entities::{update::Log, SystemCommand}; +use periphery_client::api::{ + build::*, container::*, git::*, network::*, stats::*, GetAccounts, + GetHealth, GetSecrets, GetVersion, GetVersionResponse, PruneSystem, + RunCommand, +}; +use resolver_api::{derive::Resolver, Resolve, ResolveToString}; +use serde::{Deserialize, Serialize}; + +use crate::{ + config::{accounts_response, secrets_response}, + helpers::{docker, run_monitor_command}, + State, +}; + +mod build; +mod container; +mod git; +mod network; +mod stats; + +#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[serde(tag = "type", content = "params")] +#[resolver_target(State)] +#[allow(clippy::enum_variant_names, clippy::large_enum_variant)] +pub enum PeripheryRequest { + GetVersion(GetVersion), + #[to_string_resolver] + GetHealth(GetHealth), + + // Config + #[to_string_resolver] + GetAccounts(GetAccounts), + #[to_string_resolver] + GetSecrets(GetSecrets), + + // Stats / Info + #[to_string_resolver] + GetSystemInformation(GetSystemInformation), + #[to_string_resolver] + GetSystemStats(GetSystemStats), + #[to_string_resolver] + GetSystemProcesses(GetSystemProcesses), + + // Docker + GetContainerList(GetContainerList), + GetContainerLog(GetContainerLog), + GetContainerLogSearch(GetContainerLogSearch), + GetContainerStats(GetContainerStats), + GetContainerStatsList(GetContainerStatsList), + GetNetworkList(GetNetworkList), + + // Actions + RunCommand(RunCommand), + CloneRepo(CloneRepo), + PullRepo(PullRepo), + DeleteRepo(DeleteRepo), + Build(Build), + PruneImages(PruneImages), + Deploy(Deploy), + StartContainer(StartContainer), + StopContainer(StopContainer), + RemoveContainer(RemoveContainer), + RenameContainer(RenameContainer), + PruneContainers(PruneContainers), + CreateNetwork(CreateNetwork), + DeleteNetwork(DeleteNetwork), + PruneNetworks(PruneNetworks), + PruneAll(PruneSystem), +} + +// + +#[async_trait] +impl ResolveToString for State { + #[instrument(name = "GetHealth", level = "debug", skip(self))] + async fn resolve_to_string( + &self, + _: GetHealth, + _: (), + ) -> anyhow::Result { + Ok(String::from("{}")) + } +} + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetVersion", level = "debug", skip(self))] + async fn resolve( + &self, + _: GetVersion, + _: (), + ) -> anyhow::Result { + Ok(GetVersionResponse { + version: env!("CARGO_PKG_VERSION").to_string(), + }) + } +} + +// + +#[async_trait] +impl ResolveToString for State { + #[instrument(name = "GetAccounts", level = "debug", skip(self))] + async fn resolve_to_string( + &self, + _: GetAccounts, + _: (), + ) -> anyhow::Result { + Ok(accounts_response().clone()) + } +} + +// + +#[async_trait] +impl ResolveToString for State { + #[instrument(name = "GetSecrets", level = "debug", skip(self))] + async fn resolve_to_string( + &self, + _: GetSecrets, + _: (), + ) -> anyhow::Result { + Ok(secrets_response().clone()) + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "RunCommand", skip(self))] + async fn resolve( + &self, + RunCommand { + command: SystemCommand { path, command }, + }: RunCommand, + _: (), + ) -> anyhow::Result { + tokio::spawn(async move { + let command = if path.is_empty() { + command + } else { + format!("cd {path} && {command}") + }; + run_monitor_command("run command", command).await + }) + .await + .context("failure in spawned task") + } +} + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneSystem", skip(self))] + async fn resolve( + &self, + PruneSystem {}: PruneSystem, + _: (), + ) -> anyhow::Result { + Ok(docker::prune_system().await) + } +} diff --git a/bin/periphery/src/api/network.rs b/bin/periphery/src/api/network.rs new file mode 100644 index 000000000..cc8039cc1 --- /dev/null +++ b/bin/periphery/src/api/network.rs @@ -0,0 +1,69 @@ +use async_trait::async_trait; +use monitor_client::entities::{ + server::docker_network::DockerNetwork, update::Log, +}; +use periphery_client::api::network::{ + CreateNetwork, DeleteNetwork, GetNetworkList, PruneNetworks, +}; +use resolver_api::Resolve; + +use crate::{ + helpers::docker::{self, client::docker_client}, + State, +}; + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "GetNetworkList", level = "debug", skip(self))] + async fn resolve( + &self, + _: GetNetworkList, + _: (), + ) -> anyhow::Result> { + docker_client().list_networks().await + } +} + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "CreateNetwork", skip(self))] + async fn resolve( + &self, + CreateNetwork { name, driver }: CreateNetwork, + _: (), + ) -> anyhow::Result { + Ok(docker::network::create_network(&name, driver).await) + } +} + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "DeleteNetwork", skip(self))] + async fn resolve( + &self, + DeleteNetwork { name }: DeleteNetwork, + _: (), + ) -> anyhow::Result { + Ok(docker::network::delete_network(&name).await) + } +} + +// + +#[async_trait] +impl Resolve for State { + #[instrument(name = "PruneNetworks", skip(self))] + async fn resolve( + &self, + _: PruneNetworks, + _: (), + ) -> anyhow::Result { + Ok(docker::network::prune_networks().await) + } +} diff --git a/bin/periphery/src/api/stats.rs b/bin/periphery/src/api/stats.rs new file mode 100644 index 000000000..ca72c87f1 --- /dev/null +++ b/bin/periphery/src/api/stats.rs @@ -0,0 +1,61 @@ +use anyhow::Context; +use periphery_client::api::stats::{ + GetSystemInformation, GetSystemProcesses, GetSystemStats, +}; +use resolver_api::ResolveToString; + +use crate::{stats::stats_client, State}; + +#[async_trait::async_trait] +impl ResolveToString for State { + #[instrument( + name = "GetSystemInformation", + level = "debug", + skip(self) + )] + async fn resolve_to_string( + &self, + _: GetSystemInformation, + _: (), + ) -> anyhow::Result { + let info = &stats_client().read().await.info; + serde_json::to_string(info) + .context("failed to serialize response to string") + } +} + +// + +#[async_trait::async_trait] +impl ResolveToString for State { + #[instrument(name = "GetSystemStats", level = "debug", skip(self))] + async fn resolve_to_string( + &self, + _: GetSystemStats, + _: (), + ) -> anyhow::Result { + let stats = &stats_client().read().await.stats; + serde_json::to_string(stats) + .context("failed to serialize response to string") + } +} + +// + +#[async_trait::async_trait] +impl ResolveToString for State { + #[instrument( + name = "GetSystemProcesses", + level = "debug", + skip(self) + )] + async fn resolve_to_string( + &self, + _: GetSystemProcesses, + _: (), + ) -> anyhow::Result { + let stats = &stats_client().read().await.get_processes(); + serde_json::to_string(&stats) + .context("failed to serialize response to string") + } +} diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs new file mode 100644 index 000000000..da8902ca6 --- /dev/null +++ b/bin/periphery/src/config.rs @@ -0,0 +1,76 @@ +use std::sync::OnceLock; + +use clap::Parser; +use merge_config_files::parse_config_paths; +use monitor_client::entities::{ + config::periphery::{CliArgs, Env, PeripheryConfig}, + logger::LogLevel, +}; +use serde_json::json; + +pub fn periphery_config() -> &'static PeripheryConfig { + static PERIPHERY_CONFIG: OnceLock = + OnceLock::new(); + PERIPHERY_CONFIG.get_or_init(|| { + let env: Env = envy::from_env() + .expect("failed to parse periphery environment"); + let args = CliArgs::parse(); + let mut config = parse_config_paths::( + args.config_path.unwrap_or(env.monitor_config_paths), + args.config_keyword.unwrap_or(env.monitor_config_keywords), + args + .merge_nested_config + .unwrap_or(env.monitor_merge_nested_config), + args + .extend_config_arrays + .unwrap_or(env.monitor_extend_config_arrays), + ) + .expect("failed at parsing config from paths"); + + // Overrides + config.port = env.monitor_port.unwrap_or(config.port); + config.repo_dir = env.monitor_repo_dir.unwrap_or(config.repo_dir); + config.stats_polling_rate = env + .monitor_stats_polling_rate + .unwrap_or(config.stats_polling_rate); + + // logging + config.logging.level = args + .log_level + .map(LogLevel::from) + .or(env.monitor_logging_level) + .unwrap_or(config.logging.level); + config.logging.stdio = + env.monitor_logging_stdio.unwrap_or(config.logging.stdio); + config.logging.otlp_endpoint = env + .monitor_logging_otlp_endpoint + .or(config.logging.otlp_endpoint); + config.logging.opentelemetry_service_name = env + .monitor_logging_opentelemetry_service_name + .unwrap_or(config.logging.opentelemetry_service_name); + + config.allowed_ips = + env.monitor_allowed_ips.unwrap_or(config.allowed_ips); + config.passkeys = env.monitor_passkeys.unwrap_or(config.passkeys); + + config + }) +} + +pub fn accounts_response() -> &'static String { + static ACCOUNTS_RESPONSE: OnceLock = OnceLock::new(); + ACCOUNTS_RESPONSE.get_or_init(|| json!({ + "docker": periphery_config().docker_accounts.keys().collect::>(), + "github": periphery_config().github_accounts.keys().collect::>(), + }).to_string()) +} + +pub fn secrets_response() -> &'static String { + static SECRETS_RESPONSE: OnceLock = OnceLock::new(); + SECRETS_RESPONSE.get_or_init(|| { + serde_json::to_string( + &periphery_config().secrets.keys().collect::>(), + ) + .unwrap() + }) +} diff --git a/bin/periphery/src/guard.rs b/bin/periphery/src/guard.rs new file mode 100644 index 000000000..eb084af47 --- /dev/null +++ b/bin/periphery/src/guard.rs @@ -0,0 +1,69 @@ +use std::net::SocketAddr; + +use anyhow::{anyhow, Context}; +use axum::{ + body::Body, + extract::ConnectInfo, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; +use serror::{AddStatusCode, AddStatusCodeError}; + +use crate::config::periphery_config; + +#[instrument(level = "debug")] +pub async fn guard_request_by_passkey( + req: Request, + next: Next, +) -> serror::Result { + if periphery_config().passkeys.is_empty() { + return Ok(next.run(req).await); + } + let Some(req_passkey) = req.headers().get("authorization") else { + return Err( + anyhow!("request was not sent with passkey") + .status_code(StatusCode::UNAUTHORIZED), + ); + }; + let req_passkey = req_passkey + .to_str() + .context("failed to convert passkey to str") + .status_code(StatusCode::UNAUTHORIZED)?; + if periphery_config() + .passkeys + .iter() + .any(|passkey| passkey == req_passkey) + { + Ok(next.run(req).await) + } else { + Err( + anyhow!("request passkey invalid") + .status_code(StatusCode::UNAUTHORIZED), + ) + } +} + +#[instrument(level = "debug")] +pub async fn guard_request_by_ip( + req: Request, + next: Next, +) -> serror::Result { + if periphery_config().allowed_ips.is_empty() { + return Ok(next.run(req).await); + } + let ConnectInfo(socket_addr) = req + .extensions() + .get::>() + .context("could not get ConnectionInfo of request") + .status_code(StatusCode::UNAUTHORIZED)?; + let ip = socket_addr.ip(); + if periphery_config().allowed_ips.contains(&ip) { + Ok(next.run(req).await) + } else { + Err( + anyhow!("requesting ip {ip} not allowed") + .status_code(StatusCode::UNAUTHORIZED), + ) + } +} diff --git a/bin/periphery/src/handler.rs b/bin/periphery/src/handler.rs new file mode 100644 index 000000000..d256649dc --- /dev/null +++ b/bin/periphery/src/handler.rs @@ -0,0 +1,54 @@ +use std::time::Instant; + +use anyhow::{anyhow, Context}; +use axum_extra::{headers::ContentType, TypedHeader}; +use resolver_api::Resolver; +use serror::Json; +use uuid::Uuid; + +use crate::State; + +pub async fn handler( + Json(request): Json, +) -> serror::Result<(TypedHeader, String)> { + let req_id = Uuid::new_v4(); + + let res = tokio::spawn(task(req_id, request)) + .await + .context("task handler spawn error"); + + if let Err(e) = &res { + warn!("request {req_id} spawn error: {e:#}"); + } + + Ok((TypedHeader(ContentType::json()), res??)) +} + +#[instrument(name = "PeripheryHandler")] +async fn task( + req_id: Uuid, + request: crate::api::PeripheryRequest, +) -> anyhow::Result { + info!("request {req_id} | {request:?}"); + let timer = Instant::now(); + + let res = + State + .resolve_request(request, ()) + .await + .map_err(|e| match e { + resolver_api::Error::Serialization(e) => { + anyhow!("{e:?}").context("response serialization error") + } + resolver_api::Error::Inner(e) => e, + }); + + if let Err(e) = &res { + warn!("request {req_id} error: {e:#}"); + } + + let elapsed = timer.elapsed(); + info!("request {req_id} | resolve time: {elapsed:?}"); + + res +} diff --git a/bin/periphery/src/helpers/docker/build.rs b/bin/periphery/src/helpers/docker/build.rs new file mode 100644 index 000000000..32c7bbb25 --- /dev/null +++ b/bin/periphery/src/helpers/docker/build.rs @@ -0,0 +1,155 @@ +use anyhow::Context; +use monitor_client::entities::{ + build::{Build, BuildConfig}, + optional_string, to_monitor_name, + update::Log, + EnvironmentVar, Version, +}; +use serror::serialize_error_pretty; + +use crate::{ + config::periphery_config, + helpers::{get_docker_token, run_monitor_command}, +}; + +use super::{docker_login, parse_extra_args, parse_labels}; + +#[instrument] +pub async fn prune_images() -> Log { + let command = String::from("docker image prune -a -f"); + run_monitor_command("prune images", command).await +} + +#[instrument(skip(docker_token))] +pub async fn build( + Build { + name, + config: + BuildConfig { + version, + docker_account, + docker_organization, + skip_secret_interp, + build_path, + dockerfile_path, + build_args, + labels, + extra_args, + use_buildx, + .. + }, + .. + }: &Build, + docker_token: Option, +) -> anyhow::Result> { + let mut logs = Vec::new(); + let docker_token = match ( + docker_token, + get_docker_token(&optional_string(docker_account)), + ) { + (Some(docker_token), _) => Some(docker_token), + (None, Ok(docker_token)) => docker_token, + (None, Err(e)) => { + logs.push(Log::error("build", serialize_error_pretty(&e))); + return Ok(logs); + } + }; + let name = to_monitor_name(name); + let using_account = + docker_login(&optional_string(docker_account), &docker_token) + .await + .context("failed to login to docker")?; + let build_dir = + periphery_config().repo_dir.join(&name).join(build_path); + let dockerfile_path = match optional_string(dockerfile_path) { + Some(dockerfile_path) => dockerfile_path.to_owned(), + None => "Dockerfile".to_owned(), + }; + let build_args = parse_build_args(build_args); + let labels = parse_labels(labels); + let extra_args = parse_extra_args(extra_args); + let buildx = if *use_buildx { " buildx" } else { "" }; + let image_name = get_image_name( + &name, + &optional_string(docker_account), + &optional_string(docker_organization), + ); + let image_tags = image_tags(&image_name, version); + let docker_push = if using_account { + format!(" && docker image push --all-tags {image_name}") + } else { + String::new() + }; + let command = format!( + "cd {} && docker{buildx} build{build_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{docker_push}", + build_dir.display() + ); + if *skip_secret_interp { + let build_log = + run_monitor_command("docker build", command).await; + info!("finished building docker image"); + logs.push(build_log); + } else { + let (command, replacers) = svi::interpolate_variables( + &command, + &periphery_config().secrets, + svi::Interpolator::DoubleBrackets, + ) + .context( + "failed to interpolate secrets into docker build command", + )?; + let mut build_log = + run_monitor_command("docker build", command).await; + build_log.command = + svi::replace_in_string(&build_log.command, &replacers); + build_log.stdout = + svi::replace_in_string(&build_log.stdout, &replacers); + build_log.stderr = + svi::replace_in_string(&build_log.stderr, &replacers); + logs.push(build_log); + } + Ok(logs) +} + +fn get_image_name( + name: &str, + docker_account: &Option, + docker_organization: &Option, +) -> String { + match docker_organization { + Some(docker_org) => format!("{docker_org}/{name}"), + None => match docker_account { + Some(docker_account) => { + format!("{docker_account}/{name}") + } + None => name.to_string(), + }, + } +} + +fn get_version_image_name( + image_name: &str, + version: &Version, +) -> String { + format!("{image_name}:{}", version.to_string()) +} + +fn get_latest_image_name(image_name: &str) -> String { + format!("{image_name}:latest") +} + +fn image_tags(image_name: &str, version: &Version) -> String { + format!( + " -t {} -t {}", + get_version_image_name(image_name, version), + get_latest_image_name(image_name) + ) +} + +fn parse_build_args(build_args: &[EnvironmentVar]) -> String { + build_args + .iter() + .map(|p| format!(" --build-arg {}=\"{}\"", p.variable, p.value)) + .collect::>() + .join("") +} diff --git a/bin/periphery/src/helpers/docker/client.rs b/bin/periphery/src/helpers/docker/client.rs new file mode 100644 index 000000000..a3ffa9a31 --- /dev/null +++ b/bin/periphery/src/helpers/docker/client.rs @@ -0,0 +1,91 @@ +use std::sync::OnceLock; + +use anyhow::Context; +use bollard::{container::ListContainersOptions, Docker}; +use monitor_client::entities::{ + deployment::ContainerSummary, + server::{ + docker_image::ImageSummary, docker_network::DockerNetwork, + }, +}; + +pub fn docker_client() -> &'static DockerClient { + static DOCKER_CLIENT: OnceLock = OnceLock::new(); + DOCKER_CLIENT.get_or_init(Default::default) +} + +pub struct DockerClient { + docker: Docker, +} + +impl Default for DockerClient { + fn default() -> DockerClient { + DockerClient { + docker: Docker::connect_with_local_defaults() + .expect("failed to connect to docker daemon"), + } + } +} + +impl DockerClient { + pub async fn list_containers( + &self, + ) -> anyhow::Result> { + let res = self + .docker + .list_containers(Some(ListContainersOptions:: { + all: true, + ..Default::default() + })) + .await? + .into_iter() + .map(|s| { + let info = ContainerSummary { + id: s.id.unwrap_or_default(), + name: s + .names + .context("no names on container")? + .pop() + .context("no names on container (empty vec)")? + .replace('/', ""), + image: s.image.unwrap_or(String::from("unknown")), + state: s + .state + .context("no container state")? + .parse() + .context("failed to parse container state")?, + status: s.status, + labels: s.labels.unwrap_or_default(), + }; + Ok::<_, anyhow::Error>(info) + }) + .collect::>>()?; + Ok(res) + } + + pub async fn list_networks( + &self, + ) -> anyhow::Result> { + let networks = self + .docker + .list_networks::(None) + .await? + .into_iter() + .map(|network| network.into()) + .collect(); + Ok(networks) + } + + pub async fn list_images( + &self, + ) -> anyhow::Result> { + let images = self + .docker + .list_images::(None) + .await? + .into_iter() + .map(|i| i.into()) + .collect(); + Ok(images) + } +} diff --git a/bin/periphery/src/helpers/docker/container.rs b/bin/periphery/src/helpers/docker/container.rs new file mode 100644 index 000000000..484d1e3fb --- /dev/null +++ b/bin/periphery/src/helpers/docker/container.rs @@ -0,0 +1,330 @@ +use anyhow::{anyhow, Context}; +use monitor_client::entities::{ + deployment::{ + Conversion, Deployment, DeploymentConfig, DeploymentImage, + DockerContainerStats, RestartMode, TerminationSignal, + }, + optional_string, to_monitor_name, + update::Log, + EnvironmentVar, SearchCombinator, +}; +use run_command::async_run_command; +use serror::serialize_error_pretty; + +use crate::{ + config::periphery_config, + helpers::{ + docker::{parse_extra_args, parse_labels}, + get_docker_token, run_monitor_command, + }, +}; + +use super::docker_login; + +#[instrument(level = "debug")] +pub async fn container_log(container_name: &str, tail: u64) -> Log { + let command = format!("docker logs {container_name} --tail {tail}"); + run_monitor_command("get container log", command).await +} + +#[instrument(level = "debug")] +pub async fn container_log_search( + container_name: &str, + terms: &[String], + combinator: SearchCombinator, +) -> Log { + let grep = match combinator { + SearchCombinator::Or => { + format!("grep -E '{}'", terms.join("|")) + } + SearchCombinator::And => { + format!("grep -P '^(?=.*{})'", terms.join(")(?=.*")) + } + }; + let command = + format!("docker logs {container_name} --tail 5000 2>&1 | {grep}"); + run_monitor_command("get container log grep", command).await +} + +#[instrument(level = "debug")] +pub async fn container_stats( + container_name: Option, +) -> anyhow::Result> { + let format = "--format \"{{ json . }}\""; + let container_name = match container_name { + Some(name) => format!(" {name}"), + None => "".to_string(), + }; + let command = + format!("docker stats{container_name} --no-stream {format}"); + let output = async_run_command(&command).await; + if output.success() { + let res = output + .stdout + .split('\n') + .filter(|e| !e.is_empty()) + .map(|e| { + let parsed = serde_json::from_str(e) + .context(format!("failed at parsing entry {e}"))?; + Ok(parsed) + }) + .collect::>>()?; + Ok(res) + } else { + Err(anyhow!("{}", output.stderr.replace('\n', ""))) + } +} + +#[instrument] +pub async fn prune_containers() -> Log { + let command = String::from("docker container prune -f"); + run_monitor_command("prune containers", command).await +} + +#[instrument] +pub async fn start_container(container_name: &str) -> Log { + let container_name = to_monitor_name(container_name); + let command = format!("docker start {container_name}"); + run_monitor_command("docker start", command).await +} + +#[instrument] +pub async fn stop_container( + container_name: &str, + signal: Option, + time: Option, +) -> Log { + let command = stop_container_command(container_name, signal, time); + let log = run_monitor_command("docker stop", command).await; + if log.stderr.contains("unknown flag: --signal") { + let command = stop_container_command(container_name, None, time); + let mut log = run_monitor_command("docker stop", command).await; + log.stderr = format!( + "old docker version: unable to use --signal flag{}", + if !log.stderr.is_empty() { + format!("\n\n{}", log.stderr) + } else { + String::new() + } + ); + log + } else { + log + } +} + +#[instrument] +pub async fn stop_and_remove_container( + container_name: &str, + signal: Option, + time: Option, +) -> Log { + let stop_command = + stop_container_command(container_name, signal, time); + let command = + format!("{stop_command} && docker container rm {container_name}"); + let log = + run_monitor_command("docker stop and remove", command).await; + if log.stderr.contains("unknown flag: --signal") { + let stop_command = + stop_container_command(container_name, None, time); + let command = format!( + "{stop_command} && docker container rm {container_name}" + ); + let mut log = run_monitor_command("docker stop", command).await; + log.stderr = format!( + "old docker version: unable to use --signal flag{}", + if !log.stderr.is_empty() { + format!("\n\n{}", log.stderr) + } else { + String::new() + } + ); + log + } else { + log + } +} + +fn stop_container_command( + container_name: &str, + signal: Option, + time: Option, +) -> String { + let container_name = to_monitor_name(container_name); + let signal = signal + .map(|signal| format!(" --signal {signal}")) + .unwrap_or_default(); + let time = time + .map(|time| format!(" --time {time}")) + .unwrap_or_default(); + format!("docker stop{signal}{time} {container_name}") +} + +#[instrument] +pub async fn rename_container( + curr_name: &str, + new_name: &str, +) -> Log { + let curr = to_monitor_name(curr_name); + let new = to_monitor_name(new_name); + let command = format!("docker rename {curr} {new}"); + run_monitor_command("docker rename", command).await +} + +#[instrument] +async fn pull_image(image: &str) -> Log { + let command = format!("docker pull {image}"); + run_monitor_command("docker pull", command).await +} + +#[instrument(skip(docker_token))] +pub async fn deploy( + deployment: &Deployment, + stop_signal: Option, + stop_time: Option, + docker_token: Option, +) -> Log { + let docker_token = match ( + docker_token, + get_docker_token(&optional_string( + &deployment.config.docker_account, + )), + ) { + (Some(token), _) => Some(token), + (None, Ok(token)) => token, + (None, Err(e)) => { + return Log::error("docker login", serialize_error_pretty(&e)) + } + }; + + if let Err(e) = docker_login( + &optional_string(&deployment.config.docker_account), + &docker_token, + ) + .await + { + return Log::error("docker login", serialize_error_pretty(&e)); + } + + let image = if let DeploymentImage::Image { image } = + &deployment.config.image + { + if image.is_empty() { + return Log::error( + "get image", + String::from("deployment does not have image attached"), + ); + } + image + } else { + return Log::error( + "get image", + String::from("deployment does not have image attached"), + ); + }; + + let _ = pull_image(image).await; + debug!("image pulled"); + let _ = stop_and_remove_container( + &deployment.name, + stop_signal, + stop_time, + ) + .await; + debug!("container stopped and removed"); + + let command = docker_run_command(deployment, image); + debug!("docker run command: {command}"); + + if deployment.config.skip_secret_interp { + run_monitor_command("docker run", command).await + } else { + let command = svi::interpolate_variables( + &command, + &periphery_config().secrets, + svi::Interpolator::DoubleBrackets, + ) + .context("failed to interpolate secrets into docker run command"); + if let Err(e) = command { + return Log::error("docker run", format!("{e:?}")); + } + let (command, replacers) = command.unwrap(); + let mut log = run_monitor_command("docker run", command).await; + log.command = svi::replace_in_string(&log.command, &replacers); + log.stdout = svi::replace_in_string(&log.stdout, &replacers); + log.stderr = svi::replace_in_string(&log.stderr, &replacers); + log + } +} + +pub fn docker_run_command( + Deployment { + name, + config: + DeploymentConfig { + volumes, + ports, + network, + process_args, + restart, + environment, + labels, + extra_args, + .. + }, + .. + }: &Deployment, + image: &str, +) -> String { + let name = to_monitor_name(name); + let ports = parse_conversions(ports, "-p"); + let volumes = volumes.to_owned(); + let volumes = parse_conversions(&volumes, "-v"); + let network = parse_network(network); + let restart = parse_restart(restart); + let environment = parse_environment(environment); + let labels = parse_labels(labels); + let process_args = parse_process_args(process_args); + let extra_args = parse_extra_args(extra_args); + format!("docker run -d --name {name}{ports}{volumes}{network}{restart}{environment}{labels}{extra_args} {image}{process_args}") +} + +fn parse_conversions( + conversions: &[Conversion], + flag: &str, +) -> String { + conversions + .iter() + .map(|p| format!(" {flag} {}:{}", p.local, p.container)) + .collect::>() + .join("") +} + +fn parse_environment(environment: &[EnvironmentVar]) -> String { + environment + .iter() + .map(|p| format!(" --env {}=\"{}\"", p.variable, p.value)) + .collect::>() + .join("") +} + +fn parse_network(network: &str) -> String { + format!(" --network {network}") +} + +fn parse_restart(restart: &RestartMode) -> String { + let restart = match restart { + RestartMode::OnFailure => "on-failure:10".to_string(), + _ => restart.to_string(), + }; + format!(" --restart {restart}") +} + +fn parse_process_args(process_args: &String) -> String { + if process_args.is_empty() { + String::new() + } else { + format!(" {process_args}") + } +} diff --git a/bin/periphery/src/helpers/docker/mod.rs b/bin/periphery/src/helpers/docker/mod.rs new file mode 100644 index 000000000..43b4fec0c --- /dev/null +++ b/bin/periphery/src/helpers/docker/mod.rs @@ -0,0 +1,63 @@ +use anyhow::anyhow; +use monitor_client::entities::{update::Log, EnvironmentVar}; +use run_command::async_run_command; + +use super::run_monitor_command; + +pub mod build; +pub mod client; +pub mod container; +pub mod network; + +pub fn get_docker_username_pw( + docker_account: &Option, + docker_token: &Option, +) -> anyhow::Result> { + match docker_account { + Some(docker_account) => match docker_token { + Some(docker_token) => Ok(Some((docker_account.to_owned(), docker_token.to_owned()))), + None => Err(anyhow!( + "docker token for account {docker_account} has not been configured on this client" + )), + }, + None => Ok(None), + } +} + +pub async fn docker_login( + docker_account: &Option, + docker_token: &Option, +) -> anyhow::Result { + let docker_account_u_pw = + get_docker_username_pw(docker_account, docker_token)?; + if let Some((username, password)) = &docker_account_u_pw { + let login = format!("docker login -u {username} -p {password}"); + async_run_command(&login).await; + Ok(true) + } else { + Ok(false) + } +} + +pub fn parse_extra_args(extra_args: &[String]) -> String { + let args = extra_args.join(" "); + if !args.is_empty() { + format!(" {args}") + } else { + args + } +} + +pub fn parse_labels(labels: &[EnvironmentVar]) -> String { + labels + .iter() + .map(|p| format!(" --label {}=\"{}\"", p.variable, p.value)) + .collect::>() + .join("") +} + +#[instrument] +pub async fn prune_system() -> Log { + let command = String::from("docker system prune -a -f"); + run_monitor_command("prune system", command).await +} diff --git a/bin/periphery/src/helpers/docker/network.rs b/bin/periphery/src/helpers/docker/network.rs new file mode 100644 index 000000000..04edd90cb --- /dev/null +++ b/bin/periphery/src/helpers/docker/network.rs @@ -0,0 +1,28 @@ +use monitor_client::entities::update::Log; + +use crate::helpers::run_monitor_command; + +#[instrument] +pub async fn create_network( + name: &str, + driver: Option, +) -> Log { + let driver = match driver { + Some(driver) => format!(" -d {driver}"), + None => String::new(), + }; + let command = format!("docker network create{driver} {name}"); + run_monitor_command("create network", command).await +} + +#[instrument] +pub async fn delete_network(name: &str) -> Log { + let command = format!("docker network rm {name}"); + run_monitor_command("delete network", command).await +} + +#[instrument] +pub async fn prune_networks() -> Log { + let command = String::from("docker network prune -f"); + run_monitor_command("prune networks", command).await +} diff --git a/bin/periphery/src/helpers/git.rs b/bin/periphery/src/helpers/git.rs new file mode 100644 index 000000000..13ea68437 --- /dev/null +++ b/bin/periphery/src/helpers/git.rs @@ -0,0 +1,205 @@ +use std::path::Path; + +use anyhow::Context; +use async_timing_util::unix_timestamp_ms; +use monitor_client::entities::{ + monitor_timestamp, to_monitor_name, update::Log, CloneArgs, + SystemCommand, +}; +use run_command::async_run_command; + +use crate::config::periphery_config; + +use super::{get_github_token, run_monitor_command}; + +pub async fn pull( + path: &Path, + branch: &Option, + on_pull: &Option, +) -> Vec { + let branch = match branch { + Some(branch) => branch.to_owned(), + None => "main".to_string(), + }; + + let command = + format!("cd {} && git pull origin {branch}", path.display()); + + let pull_log = run_monitor_command("git pull", command).await; + + if !pull_log.success { + return vec![pull_log]; + } + + let commit_hash_log = + get_commit_hash_log(path).await.unwrap_or(Log::simple( + "latest commit", + String::from("failed to get latest commit"), + )); + + let mut logs = vec![pull_log, commit_hash_log]; + + if let Some(on_pull) = on_pull { + if !on_pull.path.is_empty() && !on_pull.command.is_empty() { + let path = path.join(&on_pull.path); + let on_pull_log = run_monitor_command( + "on pull", + format!("cd {} && {}", path.display(), on_pull.command), + ) + .await; + logs.push(on_pull_log); + } + } + + logs +} + +#[instrument] +pub async fn clone( + clone_args: T, + github_token: Option, +) -> anyhow::Result> +where + T: Into + std::fmt::Debug, +{ + let CloneArgs { + name, + repo, + branch, + on_clone, + on_pull, + github_account, + } = clone_args.into(); + + let access_token = + match (github_token, get_github_token(&github_account)) { + (Some(token), _) => Some(token), + (None, Ok(token)) => token, + (None, Err(e)) => return Err(e), + }; + + let repo = repo.as_ref().context("build has no repo attached")?; + let name = to_monitor_name(&name); + + let repo_dir = periphery_config().repo_dir.join(name); + + let clone_log = + clone_inner(repo, &repo_dir, &branch, access_token).await; + + if !clone_log.success { + warn!("repo at {repo_dir:?} failed to clone"); + return Ok(vec![clone_log]); + } + + info!("repo at {repo_dir:?} cloned with clone_inner"); + + let commit_hash_log = get_commit_hash_log(&repo_dir).await?; + + let mut logs = vec![clone_log, commit_hash_log]; + + if let Some(command) = on_clone { + if !command.path.is_empty() && !command.command.is_empty() { + let on_clone_path = repo_dir.join(&command.path); + let on_clone_log = run_monitor_command( + "on clone", + format!( + "cd {} && {}", + on_clone_path.display(), + command.command + ), + ) + .await; + info!( + "run repo on_clone command | command: {} | cwd: {:?}", + command.command, on_clone_path + ); + logs.push(on_clone_log); + } + } + if let Some(command) = on_pull { + if !command.path.is_empty() && !command.command.is_empty() { + let on_pull_path = repo_dir.join(&command.path); + let on_pull_log = run_monitor_command( + "on pull", + format!( + "cd {} && {}", + on_pull_path.display(), + command.command + ), + ) + .await; + info!( + "run repo on_pull command | command: {} | cwd: {:?}", + command.command, on_pull_path + ); + logs.push(on_pull_log); + } + } + Ok(logs) +} + +#[instrument] +async fn clone_inner( + repo: &str, + destination: &Path, + branch: &Option, + access_token: Option, +) -> Log { + let _ = std::fs::remove_dir_all(destination); + let access_token_at = match &access_token { + Some(token) => format!("{token}@"), + None => String::new(), + }; + let branch = match branch { + Some(branch) => format!(" -b {branch}"), + None => String::new(), + }; + let repo_url = + format!("https://{access_token_at}github.com/{repo}.git"); + let command = + format!("git clone {repo_url} {}{branch}", destination.display()); + let start_ts = unix_timestamp_ms() as i64; + let output = async_run_command(&command).await; + let success = output.success(); + let (command, stderr) = if !access_token_at.is_empty() { + let access_token = access_token.unwrap(); + ( + command.replace(&access_token, ""), + output.stderr.replace(&access_token, ""), + ) + } else { + (command, output.stderr) + }; + Log { + stage: "clone repo".to_string(), + command, + success, + stdout: output.stdout, + stderr, + start_ts, + end_ts: unix_timestamp_ms() as i64, + } +} + +#[instrument] +async fn get_commit_hash_log(repo_dir: &Path) -> anyhow::Result { + let start_ts = monitor_timestamp(); + let command = format!("cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B", repo_dir.display()); + let output = async_run_command(&command).await; + let mut split = output.stdout.split('\n'); + let (short, _, msg) = ( + split.next().context("failed to get short commit hash")?, + split.next().context("failed to get long commit hash")?, + split.next().context("failed to get commit message")?, + ); + let log = Log { + stage: "latest commit".into(), + command, + stdout: format!("hash: {short}\nmessage: {msg}"), + stderr: String::new(), + success: true, + start_ts, + end_ts: monitor_timestamp(), + }; + Ok(log) +} diff --git a/bin/periphery/src/helpers/mod.rs b/bin/periphery/src/helpers/mod.rs new file mode 100644 index 000000000..38d10f2af --- /dev/null +++ b/bin/periphery/src/helpers/mod.rs @@ -0,0 +1,68 @@ +use anyhow::anyhow; +use async_timing_util::unix_timestamp_ms; +use monitor_client::entities::update::Log; +use run_command::{async_run_command, CommandOutput}; + +use crate::config::periphery_config; + +pub mod docker; +pub mod git; + +pub fn get_github_token( + github_account: &Option, +) -> anyhow::Result> { + match github_account { + Some(account) => { + match periphery_config().github_accounts.get(account) { + Some(token) => Ok(Some(token.to_owned())), + None => Err(anyhow!( + "did not find token in config for github account {account} " + )), + } + } + None => Ok(None), + } +} + +pub fn get_docker_token( + docker_account: &Option, +) -> anyhow::Result> { + match docker_account { + Some(account) => { + match periphery_config().docker_accounts.get(account) { + Some(token) => Ok(Some(token.to_owned())), + None => Err(anyhow!( + "did not find token in config for docker account {account} " + )), + } + } + None => Ok(None), + } +} + +pub async fn run_monitor_command( + stage: &str, + command: String, +) -> Log { + let start_ts = unix_timestamp_ms() as i64; + let output = async_run_command(&command).await; + output_into_log(stage, command, start_ts, output) +} + +pub fn output_into_log( + stage: &str, + command: String, + start_ts: i64, + output: CommandOutput, +) -> Log { + let success = output.success(); + Log { + stage: stage.to_string(), + stdout: output.stdout, + stderr: output.stderr, + command, + success, + start_ts, + end_ts: unix_timestamp_ms() as i64, + } +} diff --git a/bin/periphery/src/main.rs b/bin/periphery/src/main.rs new file mode 100644 index 000000000..d06349459 --- /dev/null +++ b/bin/periphery/src/main.rs @@ -0,0 +1,64 @@ +#[macro_use] +extern crate tracing; + +use std::{net::SocketAddr, str::FromStr}; + +use anyhow::Context; +use axum::{middleware, routing::post, Router}; +use termination_signal::tokio::immediate_term_handle; + +mod api; +mod config; +mod guard; +mod handler; +mod helpers; +mod stats; + +struct State; + +async fn app() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + let config = config::periphery_config(); + logger::init(&config.logging)?; + + info!("version: v{}", env!("CARGO_PKG_VERSION")); + + stats::spawn_system_stats_polling_threads(); + + let socket_addr = + SocketAddr::from_str(&format!("0.0.0.0:{}", config.port)) + .context("failed to parse socket addr")?; + + let app = Router::new() + .route("/", post(handler::handler)) + .layer(middleware::from_fn(guard::guard_request_by_ip)) + .layer(middleware::from_fn(guard::guard_request_by_passkey)); + + info!("starting server on {}", socket_addr); + + let listener = tokio::net::TcpListener::bind(&socket_addr) + .await + .context("failed to bind tcp listener")?; + + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let term_signal = immediate_term_handle()?; + + let app = tokio::spawn(app()); + + tokio::select! { + res = app => return res?, + _ = term_signal => {}, + } + + Ok(()) +} diff --git a/bin/periphery/src/stats.rs b/bin/periphery/src/stats.rs new file mode 100644 index 000000000..d60a948b6 --- /dev/null +++ b/bin/periphery/src/stats.rs @@ -0,0 +1,170 @@ +use std::{cmp::Ordering, sync::OnceLock}; + +use async_timing_util::wait_until_timelength; +use monitor_client::entities::server::stats::*; +use sysinfo::System; +use tokio::sync::RwLock; + +use crate::config::periphery_config; + +pub fn stats_client() -> &'static RwLock { + static STATS_CLIENT: OnceLock> = + OnceLock::new(); + STATS_CLIENT.get_or_init(|| RwLock::new(StatsClient::default())) +} + +/// This should be called before starting the server in main.rs. +/// Keeps the caches stats up to date +pub fn spawn_system_stats_polling_threads() { + tokio::spawn(async move { + let client = stats_client(); + loop { + let ts = wait_until_timelength( + async_timing_util::Timelength::FiveMinutes, + 0, + ) + .await; + let mut client = client.write().await; + client.refresh_lists(); + client.stats.refresh_list_ts = ts as i64; + } + }); + tokio::spawn(async move { + let polling_rate = periphery_config() + .stats_polling_rate + .to_string() + .parse() + .expect("invalid stats polling rate"); + let client = stats_client(); + loop { + let ts = wait_until_timelength(polling_rate, 1).await; + let mut client = client.write().await; + client.refresh(); + client.stats = client.get_system_stats(); + client.stats.refresh_ts = ts as i64; + } + }); +} + +pub struct StatsClient { + /// Cached system stats + pub stats: SystemStats, + /// Cached system information + pub info: SystemInformation, + + // the handles used to get the stats + system: sysinfo::System, + disks: sysinfo::Disks, +} + +const BYTES_PER_GB: f64 = 1073741824.0; +const BYTES_PER_MB: f64 = 1048576.0; +const BYTES_PER_KB: f64 = 1024.0; + +impl Default for StatsClient { + fn default() -> Self { + let system = sysinfo::System::new_all(); + let disks = sysinfo::Disks::new_with_refreshed_list(); + let stats = SystemStats { + polling_rate: periphery_config().stats_polling_rate, + ..Default::default() + }; + StatsClient { + info: get_system_information(&system), + system, + disks, + stats, + } + } +} + +impl StatsClient { + fn refresh(&mut self) { + self.system.refresh_cpu(); + self.system.refresh_memory(); + self.system.refresh_processes(); + self.disks.refresh(); + } + + fn refresh_lists(&mut self) { + self.disks.refresh_list(); + } + + pub fn get_system_stats(&self) -> SystemStats { + SystemStats { + cpu_perc: self.system.global_cpu_info().cpu_usage(), + mem_used_gb: self.system.used_memory() as f64 / BYTES_PER_GB, + mem_total_gb: self.system.total_memory() as f64 / BYTES_PER_GB, + disks: self.get_disks(), + polling_rate: self.stats.polling_rate, + refresh_ts: self.stats.refresh_ts, + refresh_list_ts: self.stats.refresh_list_ts, + } + } + + fn get_disks(&self) -> Vec { + self + .disks + .list() + .iter() + .map(|disk| { + let disk_total = disk.total_space() as f64 / BYTES_PER_GB; + let disk_free = disk.available_space() as f64 / BYTES_PER_GB; + SingleDiskUsage { + mount: disk.mount_point().to_owned(), + used_gb: disk_total - disk_free, + total_gb: disk_total, + } + }) + .collect() + } + + pub fn get_processes(&self) -> Vec { + let mut procs: Vec<_> = self + .system + .processes() + .iter() + .map(|(pid, p)| { + let disk_usage = p.disk_usage(); + SystemProcess { + pid: pid.as_u32(), + name: p.name().to_string(), + exe: p + .exe() + .map(|exe| exe.to_str().unwrap_or_default()) + .unwrap_or_default() + .to_string(), + cmd: p.cmd().to_vec(), + start_time: (p.start_time() * 1000) as f64, + cpu_perc: p.cpu_usage(), + mem_mb: p.memory() as f64 / BYTES_PER_MB, + disk_read_kb: disk_usage.read_bytes as f64 / BYTES_PER_KB, + disk_write_kb: disk_usage.written_bytes as f64 + / BYTES_PER_KB, + } + }) + .collect(); + procs.sort_by(|a, b| { + if a.cpu_perc > b.cpu_perc { + Ordering::Less + } else { + Ordering::Greater + } + }); + procs + } +} + +fn get_system_information( + sys: &sysinfo::System, +) -> SystemInformation { + let cpu = sys.global_cpu_info(); + SystemInformation { + name: System::name(), + os: System::long_os_version(), + kernel: System::kernel_version(), + host_name: System::host_name(), + core_count: sys.physical_core_count().map(|c| c as u32), + cpu_brand: cpu.brand().to_string(), + } +} diff --git a/bin/sync/Cargo.toml b/bin/sync/Cargo.toml new file mode 100644 index 000000000..8bc7393e8 --- /dev/null +++ b/bin/sync/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "monitor_sync" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +# mogh +merge_config_files.workspace = true +# +tokio.workspace = true +clap.workspace = true +anyhow.workspace = true +serde.workspace = true +dotenv.workspace = true \ No newline at end of file diff --git a/bin/sync/src/main.rs b/bin/sync/src/main.rs new file mode 100644 index 000000000..6113c5730 --- /dev/null +++ b/bin/sync/src/main.rs @@ -0,0 +1,42 @@ +#![allow(unused)] + +use std::collections::HashMap; + +use anyhow::Context; +use clap::Parser; +use monitor_client::{ + entities::{build::Build, deployment::Deployment, server::Server}, + MonitorClient, +}; +use serde::Deserialize; + +#[derive(Parser)] +#[command(author, about, version)] +pub struct MonitorSyncArgs { + /// The root path of the sync files + #[arg(short, long)] + pub path: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MonitorSyncFile { + pub servers: HashMap, + pub deployments: HashMap, + pub builds: HashMap, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + + let args = MonitorSyncArgs::parse(); + + let sync_file: MonitorSyncFile = + merge_config_files::parse_config_file(args.path).unwrap(); + + let monitor = MonitorClient::new_from_env() + .await + .context("failed to initialize monitor client")?; + + Ok(()) +} diff --git a/bin/tests/Cargo.toml b/bin/tests/Cargo.toml new file mode 100644 index 000000000..9de08d25b --- /dev/null +++ b/bin/tests/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tests" +version.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +monitor_client.workspace = true +logger.workspace = true +tokio.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +partial_derive2.workspace = true +mungos.workspace = true +dotenv.workspace = true +envy.workspace = true +rand.workspace = true +tracing.workspace = true diff --git a/bin/tests/src/core.rs b/bin/tests/src/core.rs new file mode 100644 index 000000000..391e8d252 --- /dev/null +++ b/bin/tests/src/core.rs @@ -0,0 +1,120 @@ +use monitor_client::{ + api::write::{ + CreateBuild, CreateBuilder, CreateDeployment, CreateServer, + UpdateTagsOnResource, + }, + entities::{ + build::BuildConfig, + builder::{PartialBuilderConfig, ServerBuilderConfig}, + deployment::DeploymentConfig, + server::ServerConfig, + }, + MonitorClient, +}; +use rand::Rng; + +use crate::random_string; + +#[allow(unused)] +pub async fn tests() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + + let monitor = MonitorClient::new_from_env().await?; + + let tags = (0..6).map(|_| random_string(5)).collect::>(); + + let mut rng = rand::thread_rng(); + let mut get_tags = || vec![tags[rng.gen_range(0..6)].to_string()]; + + let server_names = (0..20) + .map(|i| format!("server-{}-{}", random_string(8), i)) + .collect::>(); + + for name in &server_names { + let resource = monitor + .write(CreateServer { + name: name.to_string(), + config: ServerConfig::builder() + .address(String::new()) + .build()? + .into(), + }) + .await?; + info!("created server {}", resource.name); + monitor + .write(UpdateTagsOnResource { + target: (&resource).into(), + tags: get_tags(), + }) + .await?; + info!("updated tags on server {}", resource.name); + } + + for (i, server_name) in server_names.iter().enumerate() { + let resource = monitor + .write(CreateDeployment { + name: format!("dep-{}-{}", random_string(8), i), + config: DeploymentConfig::builder() + .server_id(server_name.to_string()) + .build()? + .into(), + }) + .await?; + info!("created deployment {}", resource.name); + monitor + .write(UpdateTagsOnResource { + target: (&resource).into(), + tags: get_tags(), + }) + .await?; + info!("updated tags on deployment {}", resource.name); + } + + let builder_names = (0..20) + .map(|i| format!("builder-{}-{}", random_string(8), i)) + .collect::>(); + + for (i, server_name) in server_names.iter().enumerate() { + let resource = monitor + .write(CreateBuilder { + name: builder_names[i].clone(), + config: PartialBuilderConfig::Server( + ServerBuilderConfig { + server_id: server_name.to_string(), + } + .into(), + ), + }) + .await?; + info!("created builder {}", resource.name); + monitor + .write(UpdateTagsOnResource { + target: (&resource).into(), + tags: get_tags(), + }) + .await?; + info!("updated tags on builder {}", resource.name); + } + + for (i, builder_name) in builder_names.iter().enumerate() { + let resource = monitor + .write(CreateBuild { + name: format!("build-{}-{}", random_string(8), i), + config: BuildConfig::builder() + .builder_id(builder_name.to_string()) + .build()? + .into(), + }) + .await?; + info!("created build {}", resource.name); + monitor + .write(UpdateTagsOnResource { + target: (&resource).into(), + tags: get_tags(), + }) + .await?; + info!("updated tags on build {}", resource.name); + } + + Ok(()) +} diff --git a/bin/tests/src/main.rs b/bin/tests/src/main.rs new file mode 100644 index 000000000..145c8c2c2 --- /dev/null +++ b/bin/tests/src/main.rs @@ -0,0 +1,24 @@ +#[macro_use] +extern crate tracing; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +mod core; +// mod periphery; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logger::init(&Default::default())?; + // periphery::tests().await?; + core::tests().await?; + + Ok(()) +} + +fn random_string(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} diff --git a/bin/tests/src/periphery.rs b/bin/tests/src/periphery.rs new file mode 100644 index 000000000..5d6100416 --- /dev/null +++ b/bin/tests/src/periphery.rs @@ -0,0 +1,53 @@ +// use periphery_client::{requests, PeripheryClient}; + +#[allow(unused)] +pub async fn tests() -> anyhow::Result<()> { + let periphery = + PeripheryClient::new("http://localhost:9001", "monitor_passkey"); + + let version = periphery.request(requests::GetVersion {}).await?; + println!("{version:?}"); + + let system_info = + periphery.request(requests::GetSystemInformation {}).await?; + println!("{system_info:#?}"); + + let processes = + periphery.request(requests::GetSystemProcesses {}).await?; + // println!("{system_stats:#?}"); + + let periphery_process = + processes.into_iter().find(|p| p.name.contains("periphery")); + println!("{periphery_process:#?}"); + + let accounts = periphery.request(requests::GetAccounts {}).await?; + println!("{accounts:#?}"); + + let secrets = periphery.request(requests::GetSecrets {}).await?; + println!("{secrets:#?}"); + + let container_stats = periphery + .request(requests::GetContainerStatsList {}) + .await?; + println!("{container_stats:#?}"); + + let res = periphery.request(requests::GetNetworkList {}).await?; + println!("{res:#?}"); + + let res = periphery + .request(requests::GetContainerStats { + name: "monitor-mongo".into(), + }) + .await?; + println!("{res:#?}"); + + let res = periphery + .request(requests::GetContainerLog { + name: "monitor-mongo".into(), + tail: 50, + }) + .await?; + println!("{res:#?}"); + + Ok(()) +} diff --git a/bin/update_logger/Cargo.toml b/bin/update_logger/Cargo.toml new file mode 100644 index 000000000..fe3277091 --- /dev/null +++ b/bin/update_logger/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "update_logger" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +logger.workspace = true +# mogh +termination_signal.workspace = true +# external +tokio.workspace = true +tracing.workspace = true +anyhow.workspace = true \ No newline at end of file diff --git a/bin/update_logger/Dockerfile b/bin/update_logger/Dockerfile new file mode 100644 index 000000000..468b18010 --- /dev/null +++ b/bin/update_logger/Dockerfile @@ -0,0 +1,14 @@ +FROM rust:1.71.1 as builder +WORKDIR /builder + +COPY . . + +RUN cargo build -p update_logger --release + +FROM gcr.io/distroless/cc + +COPY --from=builder /builder/target/release/update_logger / + +EXPOSE 7000 + +CMD ["./update_logger"] \ No newline at end of file diff --git a/bin/update_logger/src/main.rs b/bin/update_logger/src/main.rs new file mode 100644 index 000000000..669298109 --- /dev/null +++ b/bin/update_logger/src/main.rs @@ -0,0 +1,40 @@ +#[macro_use] +extern crate tracing; + +use monitor_client::MonitorClient; +use termination_signal::tokio::immediate_term_handle; + +async fn app() -> anyhow::Result<()> { + logger::init(&Default::default())?; + + info!("v {}", env!("CARGO_PKG_VERSION")); + + let monitor = MonitorClient::new_from_env().await?; + + let (mut rx, _) = monitor.subscribe_to_updates(1000, 5)?; + + loop { + let msg = rx.recv().await; + if let Err(e) = msg { + error!("🚨 recv error | {e:?}"); + break; + } + info!("{msg:?}"); + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let term_signal = immediate_term_handle()?; + + let app = tokio::spawn(app()); + + tokio::select! { + res = app => return res?, + _ = term_signal => {}, + } + + Ok(()) +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index 1aaa57cfe..000000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "monitor_cli" -version = "0.3.4" -edition = "2021" -authors = ["MoghTech"] -description = "monitor cli | tools to setup monitor system" -license = "GPL-3.0-or-later" - -[[bin]] -name = "monitor" -path = "src/main.rs" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -monitor_types = { path = "../lib/types" } -clap = "4.0" -async_timing_util = "0.1.14" -rand = "0.8" -serde = "1.0" -serde_derive = "1.0" -toml = "0.7" -run_command = "0.0.5" -colored = "2" -strum = "0.24" -strum_macros = "0.24" \ No newline at end of file diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs deleted file mode 100644 index 33ba4d002..000000000 --- a/cli/src/helpers.rs +++ /dev/null @@ -1,621 +0,0 @@ -use std::{ - env, - fs::{self, File}, - io::{Read, Write}, - net::IpAddr, - path::PathBuf, - str::FromStr, -}; - -use clap::ArgMatches; -use colored::Colorize; -use monitor_types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode, Timelength}; -use rand::{distributions::Alphanumeric, Rng}; -use run_command::run_command_pipe_to_terminal; -use serde::Serialize; - -const CORE_IMAGE_NAME: &str = "mbecker2020/monitor_core"; -const PERIPHERY_IMAGE_NAME: &str = "mbecker2020/monitor_periphery"; -const PERIPHERY_CRATE: &str = "monitor_periphery"; - -pub fn gen_core_config(sub_matches: &ArgMatches) { - let host = sub_matches - .get_one::("host") - .map(|p| p.as_str()) - .unwrap_or("http://localhost:9000") - .to_string(); - - let path = sub_matches - .get_one::("path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/core.config.toml") - .to_string(); - - let port = sub_matches - .get_one::("port") - .map(|p| p.as_str()) - .unwrap_or("9000") - .parse::() - .expect("invalid port"); - - let mongo_uri = sub_matches - .get_one::("mongo-uri") - .map(|p| p.as_str()) - .unwrap_or("mongodb://monitor-mongo") - .to_string(); - - let mongo_db_name = sub_matches - .get_one::("mongo-db-name") - .map(|p| p.as_str()) - .unwrap_or("monitor") - .to_string(); - - let jwt_valid_for = sub_matches - .get_one::("jwt-valid-for") - .map(|p| p.as_str()) - .unwrap_or("1-wk") - .parse() - .expect("invalid jwt-valid-for"); - - let slack_url = sub_matches - .get_one::("slack-url") - .map(|p| p.to_owned()); - - let config = CoreConfig { - title: String::from("monitor"), - host, - port, - jwt_valid_for, - monitoring_interval: Timelength::OneMinute, - daily_offset_hours: 0, - keep_stats_for_days: 120, - slack_url, - local_auth: true, - github_oauth: Default::default(), - google_oauth: Default::default(), - aws: Default::default(), - docker_organizations: Default::default(), - mongo: MongoConfig { - uri: mongo_uri, - db_name: mongo_db_name, - app_name: "monitor".to_string(), - }, - jwt_secret: generate_secret(40), - github_webhook_secret: generate_secret(30), - github_webhook_base_url: None, - passkey: generate_secret(30), - }; - - write_to_toml(&path, &config); - - println!( - "\n✅ {} has been generated at {path} ✅\n", - "core config".bold() - ); -} - -pub fn start_mongo(sub_matches: &ArgMatches) { - let username = sub_matches.get_one::("username"); - let password = sub_matches.get_one::("password"); - - if (username.is_some() && password.is_none()) { - println!( - "\n❌ must provide {} if username is provided ❌\n", - "--password".bold() - ); - return; - } - if (username.is_none() && password.is_some()) { - println!( - "\n❌ must provide {} if password is provided ❌\n", - "--username".bold() - ); - return; - } - - let skip_enter = *sub_matches.get_one::("yes").unwrap_or(&false); - - let name = sub_matches - .get_one::("name") - .map(|p| p.as_str()) - .unwrap_or("monitor-mongo"); - - let port = sub_matches - .get_one::("port") - .map(|p| p.as_str()) - .unwrap_or("27017") - .parse::() - .expect("invalid port"); - - let network = sub_matches - .get_one::("network") - .map(|p| p.as_str()) - .unwrap_or("bridge"); - - let mount = sub_matches - .get_one::("mount") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/db"); - - let restart = sub_matches - .get_one::("restart") - .map(|p| p.as_str()) - .unwrap_or("unless-stopped") - .parse::() - .expect("invalid restart mode"); - - let env = if let (Some(username), Some(password)) = (username, password) { - format!(" --env MONGO_INITDB_ROOT_USERNAME={username} --env MONGO_INITDB_ROOT_PASSWORD={password}") - } else { - String::new() - }; - - println!( - "\n====================\n {} \n====================\n", - "mongo config".bold() - ); - if let Some(username) = username { - println!("{}: {username}", "mongo username".dimmed()); - } - println!("{}: {name}", "container name".dimmed()); - println!("{}: {port}", "port".dimmed()); - println!("{}: {mount}", "mount".dimmed()); - println!("{}: {network}", "network".dimmed()); - println!("{}: {restart}", "restart".dimmed()); - - if !skip_enter { - println!( - "\npress {} to start {}. {}", - "ENTER".green().bold(), - "MongoDB".bold(), - "(ctrl-c to cancel)".dimmed() - ); - - let buffer = &mut [0u8]; - let res = std::io::stdin().read_exact(buffer); - - if res.is_err() { - println!("pressed another button, exiting"); - } - } - - let stop = - run_command_pipe_to_terminal(&format!("docker stop {name} && docker container rm {name}")); - - let command = format!("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); - - if output.success() { - println!("\n✅ {} has been started up ✅\n", "monitor mongo".bold()) - } else { - eprintln!("\n❌ there was some {} on startup ❌\n", "error".red()) - } -} - -pub fn start_core(sub_matches: &ArgMatches) { - let skip_enter = *sub_matches.get_one::("yes").unwrap_or(&false); - - let config_path = sub_matches - .get_one::("config-path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/core.config.toml") - .to_string(); - - let name = sub_matches - .get_one::("name") - .map(|p| p.as_str()) - .unwrap_or("monitor-core"); - - let port = sub_matches - .get_one::("port") - .map(|p| p.as_str()) - .unwrap_or("9000") - .parse::() - .expect("invalid port"); - - let network = sub_matches - .get_one::("network") - .map(|p| p.as_str()) - .unwrap_or("bridge"); - - let restart = sub_matches - .get_one::("restart") - .map(|p| p.as_str()) - .unwrap_or("unless-stopped") - .parse::() - .expect("invalid restart mode"); - - let add_host = sub_matches - .get_one::("add-internal-host") - .map(|p| *p) - .unwrap_or(true); - - println!( - "\n===================\n {} \n===================\n", - "core config".bold() - ); - println!("{}: {name}", "container name".dimmed()); - println!("{}: {config_path}", "config path".dimmed()); - println!("{}: {port}", "port".dimmed()); - println!("{}: {network}", "network".dimmed()); - println!("{}: {restart}", "restart".dimmed()); - println!("{}: {add_host}", "add internal host".dimmed()); - - if !skip_enter { - println!( - "\npress {} to start {}. {}", - "ENTER".green().bold(), - "monitor core".bold(), - "(ctrl-c to cancel)".dimmed() - ); - - let buffer = &mut [0u8]; - let res = std::io::stdin().read_exact(buffer); - - if res.is_err() { - println!("pressed another button, exiting"); - } - } - - println!("\nstarting monitor core container...\n"); - - let _ = run_command_pipe_to_terminal(&format!("docker pull {CORE_IMAGE_NAME}")); - - let _ = - run_command_pipe_to_terminal(&format!("docker stop {name} && docker container rm {name}")); - - let add_host = if add_host { - " --add-host host.docker.internal:host-gateway" - } else { - "" - }; - - let command = format!("docker run -d --name {name} -p {port}:9000 --network {network} -v {config_path}:/config/config.toml --restart {restart}{add_host} {CORE_IMAGE_NAME}"); - - let output = run_command_pipe_to_terminal(&command); - - if output.success() { - println!("\n✅ {} has been started up ✅\n", "monitor core".bold()) - } else { - eprintln!("\n❌ there was some {} on startup ❌\n", "error".red()) - } -} - -pub fn gen_periphery_config(sub_matches: &ArgMatches) { - let path = sub_matches - .get_one::("path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.config.toml") - .to_string(); - - let port = sub_matches - .get_one::("port") - .map(|p| p.as_str()) - .unwrap_or("8000") - .parse::() - .expect("invalid port"); - - let stats_polling_rate = sub_matches - .get_one::("stats-polling-rate") - .map(|p| p.as_str()) - .unwrap_or("5-sec") - .parse::() - .expect("invalid timelength"); - - let allowed_ips = sub_matches - .get_one::("allowed-ips") - .map(|p| p.as_str()) - .unwrap_or("") - .split(",") - .filter(|ip| ip.len() > 0) - .map(|ip| { - ip.parse() - .expect("given allowed ip address is not valid ip") - }) - .collect::>(); - - let repo_dir = sub_matches - .get_one::("repo-dir") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/repos") - .to_string() - .replace("~", env::var("HOME").unwrap().as_str()) - .parse() - .expect("failed to parse --repo_dir as path"); - - let config = PeripheryConfig { - port, - repo_dir, - stats_polling_rate, - allowed_ips, - passkeys: vec![], - secrets: Default::default(), - github_accounts: Default::default(), - docker_accounts: Default::default(), - }; - - write_to_toml(&path, &config); - - println!( - "\n✅ {} generated at {path} ✅\n", - "periphery config".bold() - ); -} - -pub fn start_periphery_systemd(sub_matches: &ArgMatches) { - let skip_enter = *sub_matches.get_one::("yes").unwrap_or(&false); - - let install = *sub_matches.get_one::("install").unwrap_or(&false); - - let config_path = sub_matches - .get_one::("config-path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.config.toml") - .to_string(); - - println!( - "\n========================\n {} \n========================\n", - "periphery config".bold() - ); - println!("{}: systemd", "run with".dimmed()); - println!("{}: {config_path}", "config path".dimmed()); - - if !skip_enter { - println!( - "\npress {} to start {}. {}", - "ENTER".green().bold(), - "monitor periphery".bold(), - "(ctrl-c to cancel)".dimmed() - ); - - let buffer = &mut [0u8]; - let res = std::io::stdin().read_exact(buffer); - - if res.is_err() { - println!("pressed another button, exiting"); - } - } - - if install { - install_periphery_from_crates_io(); - } - - gen_periphery_service_file(&config_path); - - let user = env::var("USER").expect("failed to find $USER env var"); - - let command = - format!("systemctl --user daemon-reload && systemctl --user enable --now periphery && loginctl enable-linger {user}"); - - let output = run_command_pipe_to_terminal(&command); - - if output.success() { - println!( - "\n✅ {} has been started up ✅\n", - "monitor periphery".bold() - ) - } else { - eprintln!("\n❌ there was some {} on startup ❌\n", "error".red()) - } -} - -pub fn start_periphery_daemon(sub_matches: &ArgMatches) { - let skip_enter = *sub_matches.get_one::("yes").unwrap_or(&false); - - let install = *sub_matches.get_one::("install").unwrap_or(&false); - - let config_path = sub_matches - .get_one::("config-path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.config.toml") - .to_string(); - - let stdout = sub_matches - .get_one::("stdout") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.log.out") - .to_string(); - - let stderr = sub_matches - .get_one::("stderr") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.log.err") - .to_string(); - - println!( - "\n========================\n {} \n========================\n", - "periphery config".bold() - ); - println!("{}: daemon", "run as".dimmed()); - println!("{}: {config_path}", "config path".dimmed()); - println!("{}: {stdout}", "stdout".dimmed()); - println!("{}: {stderr}", "stderr".dimmed()); - - if !skip_enter { - println!( - "\npress {} to start {}. {}", - "ENTER".green().bold(), - "monitor periphery".bold(), - "(ctrl-c to cancel)".dimmed() - ); - - let buffer = &mut [0u8]; - let res = std::io::stdin().read_exact(buffer); - - if res.is_err() { - println!("pressed another button, exiting"); - } - } - - if install { - install_periphery_from_crates_io(); - } - - let command = format!("if pgrep periphery; then pkill periphery; fi && periphery --daemon --config-path {config_path} --stdout {stdout} --stderr {stderr}"); - - let output = run_command_pipe_to_terminal(&command); - - if output.success() { - println!( - "\n✅ {} has been started up ✅\n", - "monitor periphery".bold() - ) - } else { - eprintln!("\n❌ there was some {} on startup ❌\n", "error".red()) - } -} - -pub fn start_periphery_container(sub_matches: &ArgMatches) { - let skip_enter = *sub_matches.get_one::("yes").unwrap_or(&false); - - let config_path = sub_matches - .get_one::("config-path") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/periphery.config.toml") - .to_string(); - - let repo_dir = sub_matches - .get_one::("repo-dir") - .map(|p| p.as_str()) - .unwrap_or("~/.monitor/repos") - .to_string(); - - let name = sub_matches - .get_one::("name") - .map(|p| p.as_str()) - .unwrap_or("monitor-periphery"); - - let port = sub_matches - .get_one::("port") - .map(|p| p.as_str()) - .unwrap_or("8000") - .parse::() - .expect("invalid port"); - - let network = sub_matches - .get_one::("network") - .map(|p| p.as_str()) - .unwrap_or("bridge"); - - let restart = sub_matches - .get_one::("restart") - .map(|p| p.as_str()) - .unwrap_or("unless-stopped") - .parse::() - .expect("invalid restart mode"); - - println!( - "\n========================\n {} \n========================\n", - "periphery config".bold() - ); - println!("{}: container", "run as".dimmed()); - println!("{}: {name}", "container name".dimmed()); - println!("{}: {config_path}", "config path".dimmed()); - println!("{}: {repo_dir}", "repo folder".dimmed()); - println!("{}: {port}", "port".dimmed()); - println!("{}: {network}", "network".dimmed()); - println!("{}: {restart}", "restart".dimmed()); - - if !skip_enter { - println!( - "\npress {} to start {}. {}", - "ENTER".green().bold(), - "monitor periphery".bold(), - "(ctrl-c to cancel)".dimmed() - ); - - let buffer = &mut [0u8]; - let res = std::io::stdin().read_exact(buffer); - - if res.is_err() { - println!("pressed another button, exiting"); - } - } - - println!("\nstarting monitor periphery container...\n"); - - let _ = run_command_pipe_to_terminal(&format!("docker pull {PERIPHERY_IMAGE_NAME}")); - - let _ = - run_command_pipe_to_terminal(&format!("docker stop {name} && docker container rm {name}")); - - let command = format!("docker run -d --name {name} -p {port}:8000 --network {network} -v {config_path}:/config/config.toml -v {repo_dir}:/repos -v /var/run/docker.sock:/var/run/docker.sock --restart {restart} {PERIPHERY_IMAGE_NAME}"); - - let output = run_command_pipe_to_terminal(&command); - - if output.success() { - println!( - "\n✅ {} has been started up ✅\n", - "monitor periphery".bold() - ) - } else { - eprintln!("\n❌ there was some {} on startup ❌\n", "error".red()) - } -} - -pub fn gen_periphery_service_file(config_path: &str) { - let home = env::var("HOME").expect("failed to find $HOME env var"); - let _ = std::fs::create_dir_all(format!("{home}/.config/systemd/user")); - let mut file = File::create(format!("{home}/.config/systemd/user/periphery.service")) - .expect("failed to create user systemd unit file"); - file.write_all(periphery_unit_file(config_path).as_bytes()) - .expect("failed to write config file"); -} - -fn write_to_toml(path: &str, toml: impl Serialize) { - let path = PathBuf::from_str(&path.replace("~", &std::env::var("HOME").unwrap())) - .expect("not a valid path"); - let _ = fs::create_dir_all(pop_path(&path)); - fs::write( - path, - toml::to_string(&toml).expect("failed to parse config into toml"), - ) - .expect("❌ failed to write toml to file ❌"); -} - -fn pop_path(path: &PathBuf) -> PathBuf { - let mut clone = path.clone(); - clone.pop(); - clone -} - -fn generate_secret(length: usize) -> String { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(length) - .map(char::from) - .collect() -} - -fn periphery_unit_file(config_path: &str) -> String { - let home = env::var("HOME").expect("failed to find $HOME env var"); - let user = env::var("USER").expect("failed to find $USER env var"); - format!( - "[Unit] -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] -WantedBy=default.target" - ) -} - -fn install_periphery_from_crates_io() { - println!("\ninstalling periphery binary...\n"); - - let install_output = run_command_pipe_to_terminal(&format!("cargo install {PERIPHERY_CRATE}")); - - if install_output.success() { - println!("\ninstallation finished, starting monitor periphery daemon\n"); - } else { - panic!( - "\n❌ there was some {} during periphery installation ❌\n", - "error".red() - ) - } -} diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index e8d0ade08..000000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,252 +0,0 @@ -#![allow(unused)] - -use clap::{arg, Arg, Command}; - -mod helpers; - -use helpers::*; - -fn cli() -> Command { - Command::new("monitor") - .about("\na cli to set up monitor components, like the periphery client") - .version(env!("CARGO_PKG_VERSION")) - .subcommand_required(true) - .arg_required_else_help(true) - .allow_external_subcommands(true) - .subcommand( - Command::new("core") - .about("tools to set up monitor core") - .subcommand_required(true) - .arg_required_else_help(true) - .allow_external_subcommands(true) - .subcommand( - Command::new("gen-config") - .about("generate a core config file") - .arg( - arg!(--host "the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'") - .required(true) - ) - .arg( - arg!(--path "sets path of generated config file. default is '~/.monitor/core.config.toml'") - .required(false) - ) - .arg( - arg!(--port "sets port core will run on. default is 9000. if running in docker, keep this port as is, set the external port when running core start command") - .required(false) - ) - .arg( - arg!(--"mongo-uri" "sets the mongo uri to use. default is 'mongodb://monitor-mongo'") - .required(false) - ) - .arg( - arg!(--"mongo-db-name" "sets the db name to use. default is 'monitor'") - .required(false) - ) - .arg( - arg!(--"jwt-valid-for" "sets the length of time jwt stays valid for. default is 1-wk (one week)") - .required(false) - ) - .arg( - arg!(--"slack-url" "sets the slack url to use for slack notifications") - .required(false) - ), - ) - .subcommand( - Command::new("start-mongo") - .about("start up a local mongo container for monitor core") - .arg( - arg!(--yes "used in scripts to skip 'enter to continue' step") - ) - .arg( - arg!(--name "specify the name of the mongo container. default is monitor-mongo") - .required(false) - ) - .arg( - arg!(--username "specify the admin username for mongo. default is mongo with no auth") - .required(false) - ) - .arg( - arg!(--password "specify the admin password for mongo. default is mongo with no auth") - .required(false) - ) - .arg( - arg!(--port "sets port mongo will run on. default is 27017") - .required(false) - ) - .arg( - arg!(--mount "sets the path the mongo data is mounted into. default is ~/.monitor/db") - .required(false) - ) - .arg( - arg!(--network "sets docker network of mongo container. default is bridge") - .required(false) - ) - .arg( - arg!(--restart "sets docker restart mode of mongo container. default is unless-stopped") - ) - ) - .subcommand( - Command::new("start") - .about("start up monitor core in container") - .arg( - arg!(--yes "used in scripts to skip 'enter to continue' step") - ) - .arg( - arg!(--name "specify the name of the monitor core container. default is monitor-core") - ) - .arg( - arg!(--"config-path" "specify the file path to use for config. default is ~/.monitor/core.config.toml") - .required(false) - ) - .arg( - arg!(--port "sets port monitor core will run on. default is 9000") - .required(false) - ) - .arg( - arg!(--network "sets docker network of monitor core container. default is bridge") - .required(false) - ) - .arg( - arg!(--restart "sets docker restart mode of monitor core container. default is unless-stopped") - ) - .arg( - arg!(--"add-internal-host" "adds the docker flag '--add-host=host.docker.internal:host-gateway'. default is true") - ) - ), - ) - .subcommand( - Command::new("periphery") - .about("tools to set up monitor periphery") - .subcommand_required(true) - .arg_required_else_help(true) - .allow_external_subcommands(true) - .subcommand( - Command::new("gen-config") - .about("generate a periphery config file") - .arg( - arg!(--path "sets path of generated config file. default is '~/.monitor/periphery.config.toml'") - .required(false) - ) - .arg( - arg!(--port "sets port periphery will run on. default is 8000. if running in docker, keep this port as is, set the external port when running periphery start command") - .required(false) - ) - .arg( - arg!(--"stats-polling-rate" "sets stats polling rate to control granularity of system stats returned. default is 5-sec. options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min") - .required(false) - ) - .arg( - arg!(--"allowed-ips" "used to only accept requests from known ips. give ips as comma seperated list, like '--allowed_ips 127.0.0.1,10.20.30.43'. default is empty, which will not block any ip.") - .required(false) - ) - .arg( - arg!(--"repo-dir" "if running in container, this should be '/repos'. default is ~/.monitor/repos").required(false) - ) - ) - .subcommand( - Command::new("start") - .about("tools to start periphery as daemon or container") - .subcommand( - Command::new("systemd") - .about("manage periphery with systemd running under current user") - .arg( - arg!(--yes "used in scripts to skip 'enter to continue' step") - ) - .arg( - arg!(--install "specify this to install periphery from crates.io") - ) - .arg( - arg!(--"config-path" "specify the file path to use for config. default is ~/.monitor/periphery.config.toml") - .required(false) - ) - ) - .subcommand( - Command::new("daemon") - .about("start up monitor periphery daemon") - .arg( - arg!(--yes "used in scripts to skip 'enter to continue' step") - ) - .arg( - arg!(--install "specify this to install periphery from crates.io") - ) - .arg( - arg!(--"config-path" "specify the file path to use for config. default is ~/.monitor/periphery.config.toml") - .required(false) - ) - .arg( - arg!(--stdout "specify the file path for periphery to log stdout to. default is ~/.monitor/periphery.log.out") - .required(false) - ) - .arg( - arg!(--stderr "specify the file path for periphery to log stderr to. default is ~/.monitor/periphery.log.err") - .required(false) - ) - ) - // .subcommand( - // Command::new("container") - // .about("start up monitor periphery in docker container") - // .arg( - // arg!(--yes "used in scripts to skip 'enter to continue' step") - // ) - // .arg( - // arg!(--name "specify the name of the monitor periphery container. default is monitor-periphery") - // ) - // .arg( - // arg!(--"config-path" "specify the file path to use for config. default is ~/.monitor/periphery.config.toml") - // .required(false) - // ) - // .arg(arg!(--"repo-dir" "specify the folder on host to clone repos into. default is ~/.monitor/repos").required(false)) - // .arg( - // arg!(--port "sets port monitor periphery will run on. default is 8000") - // .required(false) - // ) - // .arg( - // arg!(--network "sets docker network of monitor periphery container. default is bridge") - // .required(false) - // ) - // .arg( - // arg!(--restart "sets docker restart mode of monitor periphery container. default is unless-stopped") - // ) - // ) - ), - ) -} - -fn main() { - let matches = cli().get_matches(); - - match matches.subcommand() { - Some(("core", sub_matches)) => { - let core_command = sub_matches.subcommand().expect("\n❌ invalid call, should be 'monitor core ' ❌\n"); - match core_command { - ("gen-config", sub_matches) => gen_core_config(sub_matches), - ("start-mongo", sub_matches) => start_mongo(sub_matches), - ("start", sub_matches) => start_core(sub_matches), - _ => { - println!("\n❌ invalid call, should be 'monitor core ' ❌\n") - } - } - } - Some(("periphery", sub_matches)) => { - let periphery_command = sub_matches.subcommand().expect( - "\n❌ invalid call, should be 'monitor periphery ' ❌\n", - ); - match periphery_command { - ("gen-config", sub_matches) => gen_periphery_config(sub_matches), - ("start", sub_matches) => { - let periphery_start_command = sub_matches.subcommand().expect("\n❌ invalid call, should be 'monitor periphery start ' ❌\n"); - match periphery_start_command { - ("systemd", sub_matches) => start_periphery_systemd(sub_matches), - ("daemon", sub_matches) => start_periphery_daemon(sub_matches), - // ("container", sub_matches) => start_periphery_container(sub_matches), - _ => println!("\n❌ invalid call, should be 'monitor periphery start ' ❌\n") - } - } - _ => { - println!("\n❌ invalid call, should be 'monitor periphery ...' ❌\n") - } - } - } - _ => println!("\n❌ invalid call, should be 'monitor ...' ❌\n"), - } -} diff --git a/client/core/rs/Cargo.toml b/client/core/rs/Cargo.toml new file mode 100644 index 000000000..c7a638aca --- /dev/null +++ b/client/core/rs/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "monitor_client" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# mogh +serror.workspace = true +resolver_api.workspace = true +derive_empty_traits.workspace = true +partial_derive2.workspace = true +derive_variants.workspace = true +derive_default_builder.workspace = true +mungos.workspace = true +mongo_indexed.workspace = true +async_timing_util.workspace = true +# external +bollard.workspace = true +reqwest.workspace = true +anyhow.workspace = true +thiserror.workspace = true +serde.workspace = true +serde_json.workspace = true +envy.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tokio-tungstenite.workspace = true +futures.workspace = true +typeshare.workspace = true +strum.workspace = true +derive_builder.workspace = true +tracing.workspace = true +uuid.workspace = true +clap.workspace = true \ No newline at end of file diff --git a/client/core/rs/src/api/auth.rs b/client/core/rs/src/api/auth.rs new file mode 100644 index 000000000..7dc99e3cd --- /dev/null +++ b/client/core/rs/src/api/auth.rs @@ -0,0 +1,126 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::{derive::Request, HasResponse}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::user::User; + +pub trait MonitorAuthRequest: HasResponse {} + +/// JSON containing an authentication token. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct JwtResponse { + /// A token the user can use to authenticate their requests. + pub jwt: String, +} + +// + +/// Non authenticated route to see the available options +/// users have to login to monitor, eg. local auth, github, google. +/// Response: [GetLoginOptionsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorAuthRequest)] +#[response(GetLoginOptionsResponse)] +pub struct GetLoginOptions {} + +/// The response for [GetLoginOptions]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub struct GetLoginOptionsResponse { + /// Whether local auth is enabled. + pub local: bool, + /// Whether github login is enabled. + pub github: bool, + /// Whether google login is enabled. + pub google: bool, +} + +// + +/// Create a new local user account. Will fail if a user with the +/// given username already exists. +/// Response: [CreateLocalUserResponse]. +/// +/// Note. This method is only available if the core api has `local_auth` enabled. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorAuthRequest)] +#[response(CreateLocalUserResponse)] +pub struct CreateLocalUser { + /// The username for the new user. + pub username: String, + /// The password for the new user. + /// This cannot be retreived later. + pub password: String, +} + +/// Response for [CreateLocalUser]. +#[typeshare] +pub type CreateLocalUserResponse = JwtResponse; + +// + +/// Login as a local user. Will fail if the users credentials don't match +/// any local user. +/// +/// Note. This method is only available if the core api has `local_auth` enabled. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorAuthRequest)] +#[response(LoginLocalUserResponse)] +pub struct LoginLocalUser { + /// The user's username + pub username: String, + /// The user's password + pub password: String, +} + +/// The response for [LoginLocalUser] +#[typeshare] +pub type LoginLocalUserResponse = JwtResponse; + +// + +/// Exchange a single use exchange token (safe for transport in url query) +/// for a jwt. +/// Response: [ExchangeForJwtResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorAuthRequest)] +#[response(ExchangeForJwtResponse)] +pub struct ExchangeForJwt { + /// The 'exchange token' + pub token: String, +} + +/// Response for [ExchangeForJwt]. +#[typeshare] +pub type ExchangeForJwtResponse = JwtResponse; + +// + +/// Get the user extracted from the request headers. +/// Response: [User]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorAuthRequest)] +#[response(GetUserResponse)] +pub struct GetUser {} + +#[typeshare] +pub type GetUserResponse = User; + +// diff --git a/client/core/rs/src/api/execute/build.rs b/client/core/rs/src/api/execute/build.rs new file mode 100644 index 000000000..ce1f55644 --- /dev/null +++ b/client/core/rs/src/api/execute/build.rs @@ -0,0 +1,52 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{update::Update, NoData}; + +use super::MonitorExecuteRequest; + +// + +/// Executes the target build. Response: [Update]. +/// +/// 1. Get a handle to the builder. If using AWS builder, this means starting a builder ec2 instance. +/// 2. Clone the repo on the builder. If an `on_clone` commmand is given, it will be executed. +/// 3. Execute `docker build {...params}`, where params are determined using the builds configuration. +/// 4. If a dockerhub account is attached, the build will be pushed to that account. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct RunBuild { + /// Can be build id or name + pub build: String, +} + +// + +/// Cancels the target build. +/// Only does anything if the build is `building` when called. +/// Response: [Update] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(CancelBuildResponse)] +pub struct CancelBuild { + /// Can be id or name + pub build: String, +} + +#[typeshare] +pub type CancelBuildResponse = NoData; diff --git a/client/core/rs/src/api/execute/deployment.rs b/client/core/rs/src/api/execute/deployment.rs new file mode 100644 index 000000000..5137b595d --- /dev/null +++ b/client/core/rs/src/api/execute/deployment.rs @@ -0,0 +1,133 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + deployment::TerminationSignal, update::Update, +}; + +use super::MonitorExecuteRequest; + +/// Deploys the container for the target deployment. Response: [Update]. +/// +/// 1. Pulls the image onto the target server. +/// 2. If the container is already running, +/// it will be stopped and removed using `docker container rm ${container_name}`. +/// 3. The container will be run using `docker run {...params}`, +/// where params are determined by the deployment's configuration. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct Deploy { + /// Name or id + pub deployment: String, + /// Override the default termination signal specified in the deployment. + pub stop_signal: Option, + /// Override the default termination max time. + pub stop_time: Option, +} + +// + +/// Starts the container for the target deployment. Response: [Update] +/// +/// 1. Runs `docker start ${container_name}`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct StartContainer { + /// Name or id + pub deployment: String, +} + +// + +/// Stops the container for the target deployment. Response: [Update] +/// +/// 1. Runs `docker stop ${container_name}`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct StopContainer { + /// Name or id + pub deployment: String, + /// Override the default termination signal specified in the deployment. + pub signal: Option, + /// Override the default termination max time. + pub time: Option, +} + +/// Stops all deployments on the target server. Response: [Update] +/// +/// 1. Runs [StopContainer] on all deployments on the server concurrently. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct StopAllContainers { + /// Name or id + pub server: String, +} + +// + +/// Stops and removes the container for the target deployment. +/// Reponse: [Update]. +/// +/// 1. The container is stopped and removed using `docker container rm ${container_name}`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct RemoveContainer { + /// Name or id. + pub deployment: String, + /// Override the default termination signal specified in the deployment. + pub signal: Option, + /// Override the default termination max time. + pub time: Option, +} diff --git a/client/core/rs/src/api/execute/mod.rs b/client/core/rs/src/api/execute/mod.rs new file mode 100644 index 000000000..7b94fc24d --- /dev/null +++ b/client/core/rs/src/api/execute/mod.rs @@ -0,0 +1,56 @@ +use derive_variants::EnumVariants; +use resolver_api::HasResponse; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +mod build; +mod deployment; +mod procedure; +mod repo; +mod server; +mod server_template; + +pub use build::*; +pub use deployment::*; +pub use procedure::*; +pub use repo::*; +pub use server::*; +pub use server_template::*; + +use crate::entities::NoData; + +pub trait MonitorExecuteRequest: HasResponse {} + +/// A wrapper for all monitor exections. +#[typeshare] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, EnumVariants, +)] +#[variant_derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(tag = "type", content = "params")] +pub enum Execution { + /// For new executions upon instantiation + None(NoData), + + // PROCEDURE + RunProcedure(RunProcedure), + + // BUILD + RunBuild(RunBuild), + + // DEPLOYMENT + Deploy(Deploy), + StartContainer(StartContainer), + StopContainer(StopContainer), + StopAllContainers(StopAllContainers), + RemoveContainer(RemoveContainer), + + // REPO + CloneRepo(CloneRepo), + PullRepo(PullRepo), + + // SERVER + PruneDockerNetworks(PruneDockerNetworks), + PruneDockerImages(PruneDockerImages), + PruneDockerContainers(PruneDockerContainers), +} diff --git a/client/core/rs/src/api/execute/procedure.rs b/client/core/rs/src/api/execute/procedure.rs new file mode 100644 index 000000000..dc356dca2 --- /dev/null +++ b/client/core/rs/src/api/execute/procedure.rs @@ -0,0 +1,26 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::Update; + +use super::MonitorExecuteRequest; + +/// Runs the target procedure. Response: [Update] +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct RunProcedure { + /// Id or name + pub procedure: String, +} diff --git a/client/core/rs/src/api/execute/repo.rs b/client/core/rs/src/api/execute/repo.rs new file mode 100644 index 000000000..1a9e39920 --- /dev/null +++ b/client/core/rs/src/api/execute/repo.rs @@ -0,0 +1,57 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::Update; + +use super::MonitorExecuteRequest; + +// + +/// Clones the target repo. Response: [Update]. +/// +/// 1. Clones the repo on the target server using `git clone https://{$token?}@github.com/${repo} -b ${branch}`. +/// The token will only be used if a github account is specified, +/// and must be declared in the periphery configuration on the target server. +/// 2. If `on_clone` and `on_pull` are specified, they will be executed. +/// `on_clone` will be executed before `on_pull`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct CloneRepo { + /// Id or name + pub repo: String, +} + +// + +/// Pulls the target repo. Response: [Update]. +/// +/// 1. Pulls the repo on the target server using `git pull`. +/// 2. If `on_pull` is specified, it will be executed after the pull is complete. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct PullRepo { + /// Id or name + pub repo: String, +} diff --git a/client/core/rs/src/api/execute/server.rs b/client/core/rs/src/api/execute/server.rs new file mode 100644 index 000000000..34a76ed69 --- /dev/null +++ b/client/core/rs/src/api/execute/server.rs @@ -0,0 +1,74 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::Update; + +use super::MonitorExecuteRequest; + +// + +/// Prunes the docker networks on the target server. Response: [Update]. +/// +/// 1. Runs `docker network prune -f`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct PruneDockerNetworks { + /// Id or name + pub server: String, +} + +// + +/// Prunes the docker images on the target server. Response: [Update]. +/// +/// 1. Runs `docker image prune -a -f`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct PruneDockerImages { + /// Id or name + pub server: String, +} + +// + +/// Prunes the docker containers on the target server. Response: [Update]. +/// +/// 1. Runs `docker container prune -f`. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Request, + EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct PruneDockerContainers { + /// Id or name + pub server: String, +} diff --git a/client/core/rs/src/api/execute/server_template.rs b/client/core/rs/src/api/execute/server_template.rs new file mode 100644 index 000000000..2e6000daa --- /dev/null +++ b/client/core/rs/src/api/execute/server_template.rs @@ -0,0 +1,23 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::Update; + +use super::MonitorExecuteRequest; + +/// Launch an EC2 instance with the specified config. +/// Response: [Update]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorExecuteRequest)] +#[response(Update)] +pub struct LaunchServer { + /// The name of the created server. + pub name: String, + /// The server template used to define the config. + pub server_template: String, +} diff --git a/client/core/rs/src/api/mod.rs b/client/core/rs/src/api/mod.rs new file mode 100644 index 000000000..95dfa56eb --- /dev/null +++ b/client/core/rs/src/api/mod.rs @@ -0,0 +1,9 @@ +//! # Monitor core API +//! +//! Monitor core exposes an HTTP api using standard JSON serialization. +//! + +pub mod auth; +pub mod execute; +pub mod read; +pub mod write; diff --git a/client/core/rs/src/api/read/alert.rs b/client/core/rs/src/api/read/alert.rs new file mode 100644 index 000000000..01988cfe6 --- /dev/null +++ b/client/core/rs/src/api/read/alert.rs @@ -0,0 +1,74 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{alert::Alert, MongoDocument, I64, U64}; + +use super::MonitorReadRequest; + +/// Get a paginated list of alerts sorted by timestamp descending. +/// Response: [ListAlertsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListAlertsResponse)] +pub struct ListAlerts { + /// Pass a custom mongo query to filter the alerts. + /// + /// ## Example JSON + /// ``` + /// { + /// "resolved": "false", + /// "level": "CRITICAL", + /// "$or": [ + /// { + /// "target": { + /// "type": "Server", + /// "id": "6608bf89cb2a12b257ab6c09" + /// } + /// }, + /// { + /// "target": { + /// "type": "Server", + /// "id": "660a5f60b74f90d5dae45fa3" + /// } + /// } + /// ] + /// } + /// ``` + /// This will filter to only include open alerts that have CRITICAL level on those two servers. + pub query: Option, + /// Retrieve older results by incrementing the page. + /// `page: 0` is default, and returns the most recent results. + #[serde(default)] + pub page: U64, +} + +/// Response for [ListAlerts]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ListAlertsResponse { + pub alerts: Vec, + /// If more alerts exist, the next page will be given here. + /// Otherwise it will be `null` + pub next_page: Option, +} + +// + +/// Get an alert: Response: [Alert]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAlertResponse)] +pub struct GetAlert { + pub id: String, +} + +#[typeshare] +pub type GetAlertResponse = Alert; diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs new file mode 100644 index 000000000..c9bbf9a84 --- /dev/null +++ b/client/core/rs/src/api/read/alerter.rs @@ -0,0 +1,65 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::alerter::{ + Alerter, AlerterListItem, AlerterQuery, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific alerter. Response: [Alerter]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAlerterResponse)] +pub struct GetAlerter { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub alerter: String, +} + +#[typeshare] +pub type GetAlerterResponse = Alerter; + +// + +/// List alerters matching optional query. Response: [ListAlertersResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListAlertersResponse)] +pub struct ListAlerters { + /// Structured query to filter alerters. + #[serde(default)] + pub query: AlerterQuery, +} + +#[typeshare] +pub type ListAlertersResponse = Vec; + +// + +/// Gets a summary of data relating to all alerters. +/// Response: [GetAlertersSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAlertersSummaryResponse)] +pub struct GetAlertersSummary {} + +/// Response for [GetAlertersSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetAlertersSummaryResponse { + pub total: u32, +} diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs new file mode 100644 index 000000000..2158928f7 --- /dev/null +++ b/client/core/rs/src/api/read/build.rs @@ -0,0 +1,200 @@ +use std::cmp::Ordering; + +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + build::{Build, BuildActionState, BuildListItem, BuildQuery}, + Version, I64, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific build. Response: [Build]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildResponse)] +pub struct GetBuild { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub build: String, +} + +#[typeshare] +pub type GetBuildResponse = Build; + +// + +/// List builds matching optional query. Response: [ListBuildsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListBuildsResponse)] +pub struct ListBuilds { + /// optional structured query to filter builds. + #[serde(default)] + pub query: BuildQuery, +} + +#[typeshare] +pub type ListBuildsResponse = Vec; + +// + +/// Get current action state for the build. Response: [BuildActionState]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildActionStateResponse)] +pub struct GetBuildActionState { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub build: String, +} + +#[typeshare] +pub type GetBuildActionStateResponse = BuildActionState; + +// + +/// Gets a summary of data relating to all builds. +/// Response: [GetBuildsSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildsSummaryResponse)] +pub struct GetBuildsSummary {} + +/// Response for [GetBuildsSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetBuildsSummaryResponse { + /// The total number of builds in monitor. + pub total: u32, +} + +// + +/// Gets summary and timeseries breakdown of the last months build count / time for charting. +/// Response: [GetBuildMonthlyStatsResponse]. +/// +/// Note. This method is paginated. One page is 30 days of data. +/// Query for older pages by incrementing the page, starting at 0. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildMonthlyStatsResponse)] +pub struct GetBuildMonthlyStats { + /// Query for older data by incrementing the page. + /// `page: 0` is the default, and will return the most recent data. + #[serde(default)] + pub page: u32, +} + +/// Response for [GetBuildMonthlyStats]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GetBuildMonthlyStatsResponse { + pub total_time: f64, // in hours + pub total_count: f64, // number of builds + pub days: Vec, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildStatsDay { + pub time: f64, + pub count: f64, + pub ts: f64, +} + +impl GetBuildMonthlyStatsResponse { + pub fn new( + mut days: Vec, + ) -> GetBuildMonthlyStatsResponse { + days.sort_by(|a, b| { + if a.ts < b.ts { + Ordering::Less + } else { + Ordering::Greater + } + }); + let mut total_time = 0.0; + let mut total_count = 0.0; + for day in &days { + total_time += day.time; + total_count += day.count; + } + GetBuildMonthlyStatsResponse { + total_time, + total_count, + days, + } + } +} + +// + +/// Paginated endpoint for versions of the build that were built in the past and available for deployment, +/// sorted by most recent first. +/// Response: [GetBuildVersionsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildVersionsResponse)] +pub struct GetBuildVersions { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub build: String, + /// The page of data. Default is 0, which corrensponds to the most recent versions. + #[serde(default)] + pub page: u32, + /// Filter to only include versions matching this major version. + pub major: Option, + /// Filter to only include versions matching this minor version. + pub minor: Option, + /// Filter to only include versions matching this patch version. + pub patch: Option, +} + +#[typeshare] +pub type GetBuildVersionsResponse = Vec; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildVersionResponseItem { + pub version: Version, + pub ts: I64, +} + +// + +/// List the available docker organizations which can be attached to builds. +/// Response: [ListDockerOrganizationsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListDockerOrganizationsResponse)] +pub struct ListDockerOrganizations {} + +#[typeshare] +pub type ListDockerOrganizationsResponse = Vec; diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs new file mode 100644 index 000000000..a0e22f777 --- /dev/null +++ b/client/core/rs/src/api/read/builder.rs @@ -0,0 +1,91 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::builder::{ + Builder, BuilderListItem, BuilderQuery, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific builder by id or name. Response: [Builder]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuilderResponse)] +pub struct GetBuilder { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub builder: String, +} + +#[typeshare] +pub type GetBuilderResponse = Builder; + +// + +/// List builders matching structured query. Response: [ListBuildersResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListBuildersResponse)] +pub struct ListBuilders { + #[serde(default)] + pub query: BuilderQuery, +} + +#[typeshare] +pub type ListBuildersResponse = Vec; + +// + +/// Gets a summary of data relating to all builders. +/// Response: [GetBuildersSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuildersSummaryResponse)] +pub struct GetBuildersSummary {} + +/// Response for [GetBuildersSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetBuildersSummaryResponse { + /// The total number of builders. + pub total: u32, +} + +// + +/// Get the docker / github accounts which are available for use on the builder. +/// Response: [GetBuilderAvailableAccountsResponse]. +/// +/// Note. Builds using this builder can only use the docker / github accounts available in this response. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetBuilderAvailableAccountsResponse)] +pub struct GetBuilderAvailableAccounts { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub builder: String, +} + +/// The response for [GetBuilderAvailableAccounts]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetBuilderAvailableAccountsResponse { + pub github: Vec, + pub docker: Vec, +} diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs new file mode 100644 index 000000000..e34eead64 --- /dev/null +++ b/client/core/rs/src/api/read/deployment.rs @@ -0,0 +1,222 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + deployment::{ + ContainerSummary, Deployment, DeploymentActionState, + DeploymentListItem, DeploymentQuery, DockerContainerState, + DockerContainerStats, + }, + update::Log, + SearchCombinator, I64, U64, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific deployment by name or id. Response: [Deployment]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDeploymentResponse)] +pub struct GetDeployment { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, +} + +#[typeshare] +pub type GetDeploymentResponse = Deployment; + +// + +/// List deployments matching optional query. +/// Response: [ListDeploymentsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListDeploymentsResponse)] +pub struct ListDeployments { + /// optional structured query to filter deployments. + #[serde(default)] + pub query: DeploymentQuery, +} + +#[typeshare] +pub type ListDeploymentsResponse = Vec; + +// + +/// Get the container, including image / status, of the target deployment. +/// Response: [GetDeploymentContainerResponse]. +/// +/// Note. This does not hit the server directly. The status comes from an +/// in memory cache on the core, which hits the server periodically +/// to keep it up to date. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDeploymentContainerResponse)] +pub struct GetDeploymentContainer { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, +} + +/// Response for [GetDeploymentContainer]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetDeploymentContainerResponse { + pub state: DockerContainerState, + pub container: Option, +} + +// + +/// Get the deployment log's tail, split by stdout/stderr. +/// Response: [Log]. +/// +/// Note. This call will hit the underlying server directly for most up to date log. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetLogResponse)] +pub struct GetLog { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, + /// The number of lines of the log tail to include. + /// Default: 100. + /// Max: 5000. + #[serde(default = "default_tail")] + pub tail: U64, +} + +fn default_tail() -> u64 { + 50 +} + +#[typeshare] +pub type GetLogResponse = Log; + +// + +/// Search the deployment log's tail using `grep`. All lines go to stdout. +/// Response: [Log]. +/// +/// Note. This call will hit the underlying server directly for most up to date log. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(SearchLogResponse)] +pub struct SearchLog { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, + /// The terms to search for. + pub terms: Vec, + /// When searching for multiple terms, can use `AND` or `OR` combinator. + /// + /// - `AND`: Only include lines with **all** terms present in that line. + /// - `OR`: Include lines that have one or more matches in the terms. + #[serde(default)] + pub combinator: SearchCombinator, +} + +#[typeshare] +pub type SearchLogResponse = Log; + +// + +/// Get the deployment container's stats using `docker stats`. +/// Response: [DockerContainerStats]. +/// +/// Note. This call will hit the underlying server directly for most up to date stats. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDeploymentStatsResponse)] +pub struct GetDeploymentStats { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, +} + +#[typeshare] +pub type GetDeploymentStatsResponse = DockerContainerStats; + +// + +/// Get current action state for the deployment. +/// Response: [DeploymentActionState]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(DeploymentActionState)] +pub struct GetDeploymentActionState { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, +} + +#[typeshare] +pub type GetDeploymentActionStateResponse = DeploymentActionState; + +// + +/// Gets a summary of data relating to all deployments. +/// Response: [GetDeploymentsSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDeploymentsSummaryResponse)] +pub struct GetDeploymentsSummary {} + +/// Response for [GetDeploymentsSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GetDeploymentsSummaryResponse { + pub total: I64, + pub running: I64, + pub stopped: I64, + pub not_deployed: I64, + pub unknown: I64, +} + +// + +/// Gets a list of existing values used as extra args across other deployments. +/// Useful to offer suggestions. Response: [ListCommonExtraArgsResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListCommonExtraArgsResponse)] +pub struct ListCommonExtraArgs { + /// optional structured query to filter deployments. + #[serde(default)] + pub query: DeploymentQuery, +} + +#[typeshare] +pub type ListCommonExtraArgsResponse = Vec; diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs new file mode 100644 index 000000000..83837717a --- /dev/null +++ b/client/core/rs/src/api/read/mod.rs @@ -0,0 +1,86 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::{derive::Request, HasResponse}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +mod alert; +mod alerter; +mod build; +mod builder; +mod deployment; +mod permission; +mod procedure; +mod repo; +mod search; +mod server; +mod server_template; +mod tag; +mod toml; +mod update; +mod user; +mod user_group; + +pub use alert::*; +pub use alerter::*; +pub use build::*; +pub use builder::*; +pub use deployment::*; +pub use permission::*; +pub use procedure::*; +pub use repo::*; +pub use search::*; +pub use server::*; +pub use server_template::*; +pub use tag::*; +pub use toml::*; +pub use update::*; +pub use user::*; +pub use user_group::*; + +use crate::entities::Timelength; + +pub trait MonitorReadRequest: HasResponse {} + +// + +/// Get the version of the core api. +/// Response: [GetVersionResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetVersionResponse)] +pub struct GetVersion {} + +/// Response for [GetVersion]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetVersionResponse { + /// The version of the core api. + pub version: String, +} + +// + +/// Get info about the core api. +/// Response: [GetCoreInfoResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetCoreInfoResponse)] +pub struct GetCoreInfo {} + +/// Response for [GetCoreInfo]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetCoreInfoResponse { + /// The title assigned to this core api. + pub title: String, + /// The monitoring interval of this core api. + pub monitoring_interval: Timelength, + /// The github webhook base url to use with github webhooks. + pub github_webhook_base_url: String, +} diff --git a/client/core/rs/src/api/read/permission.rs b/client/core/rs/src/api/read/permission.rs new file mode 100644 index 000000000..2b38a66dd --- /dev/null +++ b/client/core/rs/src/api/read/permission.rs @@ -0,0 +1,61 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + permission::{Permission, PermissionLevel, UserTarget}, + update::ResourceTarget, +}; + +use super::MonitorReadRequest; + +/// List permissions for the calling user. +/// Does not include any permissions on UserGroups they may be a part of. +/// Response: [ListPermissionsResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListPermissionsResponse)] +pub struct ListPermissions {} + +#[typeshare] +pub type ListPermissionsResponse = Vec; + +// + +/// Gets the calling user's permission level on a specific resource. +/// Factors in any UserGroup's permissions they may be a part of. +/// Response: [PermissionLevel] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetPermissionLevelResponse)] +pub struct GetPermissionLevel { + pub target: ResourceTarget, +} + +#[typeshare] +pub type GetPermissionLevelResponse = PermissionLevel; + +// + +/// List permissions for a specific user. **Admin only**. +/// Response: [ListUserTargetPermissionsResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUserTargetPermissionsResponse)] +pub struct ListUserTargetPermissions { + /// Specify either a user or a user group. + pub user_target: UserTarget, +} + +#[typeshare] +pub type ListUserTargetPermissionsResponse = Vec; diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs new file mode 100644 index 000000000..1bbb18324 --- /dev/null +++ b/client/core/rs/src/api/read/procedure.rs @@ -0,0 +1,85 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::procedure::{ + Procedure, ProcedureActionState, ProcedureListItem, ProcedureQuery, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific procedure. Response: [Procedure]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProcedureResponse)] +pub struct GetProcedure { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub procedure: String, +} + +#[typeshare] +pub type GetProcedureResponse = Procedure; + +// + +/// List procedures matching optional query. Response: [ListProceduresResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListProceduresResponse)] +pub struct ListProcedures { + /// optional structured query to filter procedures. + #[serde(default)] + pub query: ProcedureQuery, +} + +#[typeshare] +pub type ListProceduresResponse = Vec; + +// + +/// Get current action state for the procedure. Response: [ProcedureActionState]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProcedureActionStateResponse)] +pub struct GetProcedureActionState { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub procedure: String, +} + +#[typeshare] +pub type GetProcedureActionStateResponse = ProcedureActionState; + +// + +/// Gets a summary of data relating to all procedures. +/// Response: [GetProceduresSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProceduresSummaryResponse)] +pub struct GetProceduresSummary {} + +/// Response for [GetProceduresSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetProceduresSummaryResponse { + pub total: u32, +} + +// diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs new file mode 100644 index 000000000..3ec54541f --- /dev/null +++ b/client/core/rs/src/api/read/repo.rs @@ -0,0 +1,83 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::repo::{ + Repo, RepoActionState, RepoListItem, RepoQuery, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific repo. Response: [Repo]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(Repo)] +pub struct GetRepo { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub repo: String, +} + +#[typeshare] +pub type GetRepoResponse = Repo; + +// + +/// List repos matching optional query. Response: [ListReposResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(Vec)] +pub struct ListRepos { + /// optional structured query to filter repos. + #[serde(default)] + pub query: RepoQuery, +} + +#[typeshare] +pub type ListReposResponse = Vec; + +// + +/// Get current action state for the repo. Response: [RepoActionState]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetRepoActionStateResponse)] +pub struct GetRepoActionState { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub repo: String, +} + +#[typeshare] +pub type GetRepoActionStateResponse = RepoActionState; + +// + +/// Gets a summary of data relating to all repos. +/// Response: [GetReposSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetReposSummaryResponse)] +pub struct GetReposSummary {} + +/// Response for [GetReposSummary] +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetReposSummaryResponse { + pub total: u32, +} diff --git a/client/core/rs/src/api/read/search.rs b/client/core/rs/src/api/read/search.rs new file mode 100644 index 000000000..59e6973b0 --- /dev/null +++ b/client/core/rs/src/api/read/search.rs @@ -0,0 +1,47 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + build::BuildListItem, deployment::DeploymentListItem, + procedure::ProcedureListItem, repo::RepoListItem, + server::ServerListItem, update::ResourceTargetVariant, + MongoDocument, +}; + +use super::MonitorReadRequest; + +// + +/// Find resources matching a common query. Response: [FindResourcesResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(FindResourcesResponse)] +pub struct FindResources { + /// The mongo query as JSON + #[serde(default)] + pub query: MongoDocument, + /// The resource variants to include in the response. + #[serde(default)] + pub resources: Vec, +} + +/// Response for [FindResources]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct FindResourcesResponse { + /// The matching servers. + pub servers: Vec, + /// The matching deployments. + pub deployments: Vec, + /// The matching builds. + pub builds: Vec, + /// The matching repos. + pub repos: Vec, + /// The matching procedures. + pub procedures: Vec, +} diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs new file mode 100644 index 000000000..bd97ce0be --- /dev/null +++ b/client/core/rs/src/api/read/server.rs @@ -0,0 +1,346 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + deployment::ContainerSummary, + server::{ + docker_image::ImageSummary, + docker_network::DockerNetwork, + stats::{ + SystemInformation, SystemProcess, SystemStats, + SystemStatsRecord, + }, + Server, ServerActionState, ServerListItem, ServerQuery, + ServerStatus, + }, + Timelength, I64, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific server. Response: [Server]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(Server)] +pub struct GetServer { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetServerResponse = Server; + +// + +/// List servers matching optional query. Response: [ListServersResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListServersResponse)] +pub struct ListServers { + /// optional structured query to filter servers. + #[serde(default)] + pub query: ServerQuery, +} + +#[typeshare] +pub type ListServersResponse = Vec; + +// + +/// Get the status of the target server. Response: [GetServerStatusResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetServerStatusResponse)] +pub struct GetServerStatus { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +/// The status for [GetServerStatus]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetServerStatusResponse { + /// The server status. + pub status: ServerStatus, +} + +// + +/// Get current action state for the servers. Response: [ServerActionState]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ServerActionState)] +pub struct GetServerActionState { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetServerActionStateResponse = ServerActionState; + +// + +/// Get the version of the monitor periphery agent on the target server. +/// Response: [GetPeripheryVersionResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetPeripheryVersionResponse)] +pub struct GetPeripheryVersion { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +/// Response for [GetPeripheryVersion]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetPeripheryVersionResponse { + /// The version of periphery. + pub version: String, +} + +// + +/// Get the docker networks on the server. Response: [GetDockerNetworksResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDockerNetworksResponse)] +pub struct GetDockerNetworks { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetDockerNetworksResponse = Vec; + +// + +/// Get the docker images locally cached on the target server. +/// Response: [GetDockerImagesResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDockerImagesResponse)] +pub struct GetDockerImages { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetDockerImagesResponse = Vec; + +// + +/// Get all docker containers on the target server. +/// Response: [GetDockerContainersResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetDockerContainersResponse)] +pub struct GetDockerContainers { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetDockerContainersResponse = Vec; + +// + +/// Get the system information of the target server. +/// Response: [SystemInformation]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetSystemInformationResponse)] +pub struct GetSystemInformation { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetSystemInformationResponse = SystemInformation; + +// + +/// Get the system stats on the target server. Response: [SystemStats]. +/// +/// Note. This does not hit the server directly. The stats come from an +/// in memory cache on the core, which hits the server periodically +/// to keep it up to date. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetSystemStatsResponse)] +pub struct GetSystemStats { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetSystemStatsResponse = SystemStats; + +// + +/// Get the processes running on the target server. +/// Response: [GetSystemProcessesResponse]. +/// +/// Note. This does not hit the server directly. The procedures come from an +/// in memory cache on the core, which hits the server periodically +/// to keep it up to date. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetSystemProcessesResponse)] +pub struct GetSystemProcesses { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetSystemProcessesResponse = Vec; + +// + +/// Paginated endpoint serving historical (timeseries) server stats for graphing. +/// Response: [GetHistoricalServerStatsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetHistoricalServerStatsResponse)] +pub struct GetHistoricalServerStats { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, + /// The granularity of the data. + pub granularity: Timelength, + /// Page of historical data. Default is 0, which is the most recent data. + /// Use with the `next_page` field of the response. + #[serde(default)] + pub page: u32, +} + +/// Response to [GetHistoricalServerStats]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetHistoricalServerStatsResponse { + /// The timeseries page of data. + pub stats: Vec, + /// If there is a next page of data, pass this to `page` to get it. + pub next_page: Option, +} + +// + +/// Gets a summary of data relating to all servers. +/// Response: [GetServersSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetServersSummaryResponse)] +pub struct GetServersSummary {} + +/// Response for [GetServersSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GetServersSummaryResponse { + /// The total number of servers. + pub total: I64, + /// The number of healthy (`status: OK`) servers. + pub healthy: I64, + /// The number of unhealthy servers. + pub unhealthy: I64, + /// The number of disabled servers. + pub disabled: I64, +} + +// + +/// Get the usernames for the available github / docker accounts +/// on the target server. +/// Response: [GetAvailableAccountsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAvailableAccountsResponse)] +pub struct GetAvailableAccounts { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +/// Response for [GetAvailableAccounts]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetAvailableAccountsResponse { + /// The github usernames + pub github: Vec, + /// The docker usernames. + pub docker: Vec, +} + +// + +/// Get the keys for available secrets on the target server. +/// Response: [GetAvailableSecretsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAvailableSecretsResponse)] +pub struct GetAvailableSecrets { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server: String, +} + +#[typeshare] +pub type GetAvailableSecretsResponse = Vec; diff --git a/client/core/rs/src/api/read/server_template.rs b/client/core/rs/src/api/read/server_template.rs new file mode 100644 index 000000000..a40e8549d --- /dev/null +++ b/client/core/rs/src/api/read/server_template.rs @@ -0,0 +1,65 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::server_template::{ + ServerTemplate, ServerTemplateListItem, ServerTemplateQuery, +}; + +use super::MonitorReadRequest; + +// + +/// Get a specific server template by id or name. Response: [ServerTemplate]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetServerTemplateResponse)] +pub struct GetServerTemplate { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub server_template: String, +} + +#[typeshare] +pub type GetServerTemplateResponse = ServerTemplate; + +// + +/// List server templates matching structured query. Response: [ListServerTemplatesResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListServerTemplatesResponse)] +pub struct ListServerTemplates { + #[serde(default)] + pub query: ServerTemplateQuery, +} + +#[typeshare] +pub type ListServerTemplatesResponse = Vec; + +// + +/// Gets a summary of data relating to all server templates. +/// Response: [GetServerTemplatesSummaryResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetServerTemplatesSummaryResponse)] +pub struct GetServerTemplatesSummary {} + +/// Response for [GetServerTemplatesSummary]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetServerTemplatesSummaryResponse { + /// The total number of server templates. + pub total: u32, +} diff --git a/client/core/rs/src/api/read/tag.rs b/client/core/rs/src/api/read/tag.rs new file mode 100644 index 000000000..7c8ad2293 --- /dev/null +++ b/client/core/rs/src/api/read/tag.rs @@ -0,0 +1,43 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{tag::Tag, MongoDocument}; + +use super::MonitorReadRequest; + +// + +/// Get data for a specific tag. Response [Tag]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetTagResponse)] +pub struct GetTag { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub tag: String, +} + +#[typeshare] +pub type GetTagResponse = Tag; + +// + +/// List data for tags matching optional mongo query. +/// Response: [ListTagsResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListTagsResponse)] +pub struct ListTags { + pub query: Option, +} + +#[typeshare] +pub type ListTagsResponse = Vec; diff --git a/client/core/rs/src/api/read/toml.rs b/client/core/rs/src/api/read/toml.rs new file mode 100644 index 000000000..125c94642 --- /dev/null +++ b/client/core/rs/src/api/read/toml.rs @@ -0,0 +1,51 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::ResourceTarget; + +use super::MonitorReadRequest; + +/// Response containing pretty formatted toml contents. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TomlResponse { + pub toml: String, +} + +// + +/// Get pretty formatted monrun sync toml for all resources +/// which the user has permissions to view. +/// Response: [TomlResponse]. +#[typeshare] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ExportAllResourcesToTomlResponse)] +pub struct ExportAllResourcesToToml {} + +#[typeshare] +pub type ExportAllResourcesToTomlResponse = TomlResponse; + +// + +/// Get pretty formatted monrun sync toml for specific resources and user groups. +/// Response: [TomlResponse]. +#[typeshare] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ExportResourcesToTomlResponse)] +pub struct ExportResourcesToToml { + /// The targets to include in the export. + pub targets: Vec, + /// The user group names or ids to include in the export. + pub user_groups: Vec, +} + +#[typeshare] +pub type ExportResourcesToTomlResponse = TomlResponse; diff --git a/client/core/rs/src/api/read/update.rs b/client/core/rs/src/api/read/update.rs new file mode 100644 index 000000000..1ef757276 --- /dev/null +++ b/client/core/rs/src/api/read/update.rs @@ -0,0 +1,55 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + update::{Update, UpdateListItem}, + MongoDocument, +}; + +use super::MonitorReadRequest; + +/// Get all data for the target update. +/// Response: [Update]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetUpdateResponse)] +pub struct GetUpdate { + pub id: String, +} + +#[typeshare] +pub type GetUpdateResponse = Update; + +// + +/// Paginated endpoint for updates matching optional query. +/// More recent updates will be returned first. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUpdatesResponse)] +pub struct ListUpdates { + /// An optional mongo query to filter the updates. + pub query: Option, + /// Page of updates. Default is 0, which is the most recent data. + /// Use with the `next_page` field of the response. + #[serde(default)] + pub page: u32, +} + +/// Response for [ListUpdates]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ListUpdatesResponse { + /// The page of updates, sorted by timestamp descending. + pub updates: Vec, + /// If there is a next page of data, pass this to `page` to get it. + pub next_page: Option, +} diff --git a/client/core/rs/src/api/read/user.rs b/client/core/rs/src/api/read/user.rs new file mode 100644 index 000000000..39bd85385 --- /dev/null +++ b/client/core/rs/src/api/read/user.rs @@ -0,0 +1,80 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{api_key::ApiKey, user::User}; + +use super::MonitorReadRequest; + +/// Gets list of api keys for the calling user. +/// Response: [ListApiKeysResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListApiKeysResponse)] +pub struct ListApiKeys {} + +#[typeshare] +pub type ListApiKeysResponse = Vec; + +// + +/// Gets list of api keys for the user at ID. +/// **Admin only.** +/// Will still fail if you call for a user_id that isn't a service user. +/// Response: [ListApiKeysForServiceUserResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListApiKeysForServiceUserResponse)] +pub struct ListApiKeysForServiceUser { + /// The id of the user. + pub user_id: String, +} + +#[typeshare] +pub type ListApiKeysForServiceUserResponse = Vec; + +// + +/// Gets list of monitor users. +/// **Admin only.** +/// Response: [ListUsersResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUsersResponse)] +pub struct ListUsers {} + +#[typeshare] +pub type ListUsersResponse = Vec; + +// + +/// Gets the username of a specific user. +/// Response: [GetUsernameResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetUsernameResponse)] +pub struct GetUsername { + /// The id of the user. + pub user_id: String, +} + +/// Response for [GetUsername]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetUsernameResponse { + /// The username of the user. + pub username: String, +} diff --git a/client/core/rs/src/api/read/user_group.rs b/client/core/rs/src/api/read/user_group.rs new file mode 100644 index 000000000..11d87cbee --- /dev/null +++ b/client/core/rs/src/api/read/user_group.rs @@ -0,0 +1,41 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::user_group::UserGroup; + +use super::MonitorReadRequest; + +/// Get a specific user group by name or id. +/// Response: [UserGroup]. +#[typeshare] +#[derive( + Debug, Clone, Serialize, Deserialize, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetUserGroupResponse)] +pub struct GetUserGroup { + /// Name or Id + pub user_group: String, +} + +#[typeshare] +pub type GetUserGroupResponse = UserGroup; + +// + +/// List all user groups which user can see. Response: [ListUserGroupsResponse]. +/// +/// Admins can see all user groups, +/// and users can see user groups to which they belong. +#[typeshare] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUserGroupsResponse)] +pub struct ListUserGroups {} + +#[typeshare] +pub type ListUserGroupsResponse = Vec; diff --git a/client/core/rs/src/api/write/alerter.rs b/client/core/rs/src/api/write/alerter.rs new file mode 100644 index 000000000..0a22a9c03 --- /dev/null +++ b/client/core/rs/src/api/write/alerter.rs @@ -0,0 +1,77 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::alerter::{Alerter, PartialAlerterConfig}; + +use super::MonitorWriteRequest; + +// + +/// Create an alerter. Response: [Alerter]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Alerter)] +pub struct CreateAlerter { + /// The name given to newly created alerter. + pub name: String, + /// Optional partial config to initialize the alerter with. + pub config: PartialAlerterConfig, +} + +// + +/// Creates a new alerter with given `name` and the configuration +/// of the alerter at the given `id`. Response: [Alerter]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Alerter)] +pub struct CopyAlerter { + /// The name of the new alerter. + pub name: String, + /// The id of the alerter to copy. + pub id: String, +} + +// + +/// Deletes the alerter at the given id, and returns the deleted alerter. +/// Response: [Alerter] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Alerter)] +pub struct DeleteAlerter { + /// The id of the alerter to delete. + pub id: String, +} + +// + +/// Update the alerter at the given id, and return the updated alerter. Response: [Alerter]. +/// +/// Note. This method updates only the fields which are set in the [PartialAlerterConfig], +/// effectively merging diffs into the final document. This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Alerter)] +pub struct UpdateAlerter { + /// The id of the alerter to update. + pub id: String, + /// The partial config update to apply. + pub config: PartialAlerterConfig, +} diff --git a/client/core/rs/src/api/write/api_key.rs b/client/core/rs/src/api/write/api_key.rs new file mode 100644 index 000000000..1d940f9a1 --- /dev/null +++ b/client/core/rs/src/api/write/api_key.rs @@ -0,0 +1,104 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{NoData, I64}; + +use super::MonitorWriteRequest; + +// + +/// Create an api key for the calling user. +/// Response: [CreateApiKeyResponse]. +/// +/// Note. After the response is served, there will be no way +/// to get the secret later. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CreateApiKeyResponse)] +pub struct CreateApiKey { + /// The name for the api key. + pub name: String, + + /// A unix timestamp in millseconds specifying api key expire time. + /// Default is 0, which means no expiry. + #[serde(default)] + pub expires: I64, +} + +/// Response for [CreateApiKey]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CreateApiKeyResponse { + /// X-API-KEY + pub key: String, + + /// X-API-SECRET + /// + /// Note. + /// There is no way to get the secret again after it is distributed in this message + pub secret: String, +} + +// + +/// Delete an api key for the calling user. +/// Response: [NoData] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(DeleteApiKeyResponse)] +pub struct DeleteApiKey { + /// The key which the user intends to delete. + pub key: String, +} + +#[typeshare] +pub type DeleteApiKeyResponse = NoData; + +// + +/// Admin only method to create an api key for a service user. +/// Response: [CreateApiKeyResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CreateApiKeyForServiceUserResponse)] +pub struct CreateApiKeyForServiceUser { + /// Must be service user + pub user_id: String, + /// The name for the api key + pub name: String, + /// A unix timestamp in millseconds specifying api key expire time. + /// Default is 0, which means no expiry. + #[serde(default)] + pub expires: I64, +} + +#[typeshare] +pub type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; + +// + +/// Admin only method to delete an api key for a service user. +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(DeleteApiKeyForServiceUserResponse)] +pub struct DeleteApiKeyForServiceUser { + pub key: String, +} + +#[typeshare] +pub type DeleteApiKeyForServiceUserResponse = NoData; diff --git a/client/core/rs/src/api/write/build.rs b/client/core/rs/src/api/write/build.rs new file mode 100644 index 000000000..6f95b58ce --- /dev/null +++ b/client/core/rs/src/api/write/build.rs @@ -0,0 +1,79 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::build::{Build, _PartialBuildConfig}; + +use super::MonitorWriteRequest; + +// + +/// Create a build. Response: [Build]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Build)] +pub struct CreateBuild { + /// The name given to newly created build. + pub name: String, + /// Optional partial config to initialize the build with. + pub config: _PartialBuildConfig, +} + +// + +/// Creates a new build with given `name` and the configuration +/// of the build at the given `id`. Response: [Build]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Build)] +pub struct CopyBuild { + /// The name of the new build. + pub name: String, + /// The id of the build to copy. + pub id: String, +} + +// + +/// Deletes the build at the given id, and returns the deleted build. +/// Response: [Build] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Build)] +pub struct DeleteBuild { + /// The id of the build to delete. + pub id: String, +} + +// + +/// Update the build at the given id, and return the updated build. +/// Response: [Build]. +/// +/// Note. This method updates only the fields which are set in the [_PartialBuildConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Build)] +pub struct UpdateBuild { + /// The id of the build to update. + pub id: String, + /// The partial config update to apply. + pub config: _PartialBuildConfig, +} diff --git a/client/core/rs/src/api/write/builder.rs b/client/core/rs/src/api/write/builder.rs new file mode 100644 index 000000000..153eaeb05 --- /dev/null +++ b/client/core/rs/src/api/write/builder.rs @@ -0,0 +1,79 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::builder::{Builder, PartialBuilderConfig}; + +use super::MonitorWriteRequest; + +// + +/// Create a builder. Response: [Builder]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Builder)] +pub struct CreateBuilder { + /// The name given to newly created builder. + pub name: String, + /// Optional partial config to initialize the builder with. + pub config: PartialBuilderConfig, +} + +// + +/// Creates a new builder with given `name` and the configuration +/// of the builder at the given `id`. Response: [Builder] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Builder)] +pub struct CopyBuilder { + /// The name of the new builder. + pub name: String, + /// The id of the builder to copy. + pub id: String, +} + +// + +/// Deletes the builder at the given id, and returns the deleted builder. +/// Response: [Builder] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Builder)] +pub struct DeleteBuilder { + /// The id of the builder to delete. + pub id: String, +} + +// + +/// Update the builder at the given id, and return the updated builder. +/// Response: [Builder]. +/// +/// Note. This method updates only the fields which are set in the [PartialBuilderConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Builder)] +pub struct UpdateBuilder { + /// The id of the builder to update. + pub id: String, + /// The partial config update to apply. + pub config: PartialBuilderConfig, +} diff --git a/client/core/rs/src/api/write/deployment.rs b/client/core/rs/src/api/write/deployment.rs new file mode 100644 index 000000000..d7a7c1af0 --- /dev/null +++ b/client/core/rs/src/api/write/deployment.rs @@ -0,0 +1,99 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + deployment::{Deployment, _PartialDeploymentConfig}, + update::Update, +}; + +use super::MonitorWriteRequest; + +// + +/// Create a deployment. Response: [Deployment]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Deployment)] +pub struct CreateDeployment { + /// The name given to newly created deployment. + pub name: String, + /// Optional partial config to initialize the deployment with. + pub config: _PartialDeploymentConfig, +} + +// + +/// Creates a new deployment with given `name` and the configuration +/// of the deployment at the given `id`. Response: [Deployment] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Deployment)] +pub struct CopyDeployment { + /// The name of the new deployment. + pub name: String, + /// The id of the deployment to copy. + pub id: String, +} + +// + +/// Deletes the deployment at the given id, and returns the deleted deployment. +/// Response: [Deployment]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Deployment)] +pub struct DeleteDeployment { + /// The id of the deployment to delete. + pub id: String, +} + +// + +/// Update the deployment at the given id, and return the updated deployment. +/// Response: [Deployment]. +/// +/// Note. This method updates only the fields which are set in the [_PartialDeploymentConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Deployment)] +pub struct UpdateDeployment { + pub id: String, + pub config: _PartialDeploymentConfig, +} + +// + +/// Rename the deployment at id to the given name. Response: [Update]. +/// +/// Note. If a container is created for the deployment, it will be renamed using +/// `docker rename ...`. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Update)] +pub struct RenameDeployment { + /// The id of the deployment to rename. + pub id: String, + /// The new name. + pub name: String, +} diff --git a/client/core/rs/src/api/write/description.rs b/client/core/rs/src/api/write/description.rs new file mode 100644 index 000000000..b3dfadd5f --- /dev/null +++ b/client/core/rs/src/api/write/description.rs @@ -0,0 +1,24 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{update::ResourceTarget, NoData}; + +use super::MonitorWriteRequest; + +/// Update a resources description. +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateDescriptionResponse)] +pub struct UpdateDescription { + pub target: ResourceTarget, + pub description: String, +} + +#[typeshare] +pub type UpdateDescriptionResponse = NoData; diff --git a/client/core/rs/src/api/write/launch.rs b/client/core/rs/src/api/write/launch.rs new file mode 100644 index 000000000..8090932b4 --- /dev/null +++ b/client/core/rs/src/api/write/launch.rs @@ -0,0 +1,23 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::update::Update; + +use super::MonitorWriteRequest; + +/// Launch an EC2 instance with the specified config. +/// Response: [Update]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Update)] +pub struct LaunchServer { + /// The name of the created server. + pub name: String, + /// The server template used to define the config. + pub server_template: String, +} diff --git a/client/core/rs/src/api/write/mod.rs b/client/core/rs/src/api/write/mod.rs new file mode 100644 index 000000000..a618bf2e4 --- /dev/null +++ b/client/core/rs/src/api/write/mod.rs @@ -0,0 +1,33 @@ +mod alerter; +mod api_key; +mod build; +mod builder; +mod deployment; +mod description; +mod launch; +mod permissions; +mod procedure; +mod repo; +mod server; +mod server_template; +mod tags; +mod user; +mod user_group; + +pub use alerter::*; +pub use api_key::*; +pub use build::*; +pub use builder::*; +pub use deployment::*; +pub use description::*; +pub use launch::*; +pub use permissions::*; +pub use procedure::*; +pub use repo::*; +pub use server::*; +pub use server_template::*; +pub use tags::*; +pub use user::*; +pub use user_group::*; + +pub trait MonitorWriteRequest: resolver_api::HasResponse {} diff --git a/client/core/rs/src/api/write/permissions.rs b/client/core/rs/src/api/write/permissions.rs new file mode 100644 index 000000000..4fa6ac6a8 --- /dev/null +++ b/client/core/rs/src/api/write/permissions.rs @@ -0,0 +1,56 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + permission::{PermissionLevel, UserTarget}, + update::ResourceTarget, + NoData, +}; + +use super::MonitorWriteRequest; + +/// Update a user or user groups permission on a resource. +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdatePermissionOnTargetResponse)] +pub struct UpdatePermissionOnTarget { + /// Specify the user or user group. + pub user_target: UserTarget, + /// Specify the target resource. + pub resource_target: ResourceTarget, + /// Specify the permission level. + pub permission: PermissionLevel, +} + +#[typeshare] +pub type UpdatePermissionOnTargetResponse = NoData; + +// + +/// Update a user's "base" permissions, eg. "enabled". +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateUserBasePermissionsResponse)] +pub struct UpdateUserBasePermissions { + /// The target user. + pub user_id: String, + /// If specified, will update users enabled state. + pub enabled: Option, + /// If specified, will update user's ability to create servers. + pub create_servers: Option, + /// If specified, will update user's ability to create builds. + pub create_builds: Option, +} + +#[typeshare] +pub type UpdateUserBasePermissionsResponse = NoData; diff --git a/client/core/rs/src/api/write/procedure.rs b/client/core/rs/src/api/write/procedure.rs new file mode 100644 index 000000000..efa423517 --- /dev/null +++ b/client/core/rs/src/api/write/procedure.rs @@ -0,0 +1,91 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::procedure::{ + Procedure, _PartialProcedureConfig, +}; + +use super::MonitorWriteRequest; + +// + +/// Create a procedure. Response: [Procedure]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CreateProcedureResponse)] +pub struct CreateProcedure { + /// The name given to newly created build. + pub name: String, + /// Optional partial config to initialize the procedure with. + pub config: _PartialProcedureConfig, +} + +#[typeshare] +pub type CreateProcedureResponse = Procedure; + +// + +/// Creates a new procedure with given `name` and the configuration +/// of the procedure at the given `id`. Response: [Procedure]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CopyProcedureResponse)] +pub struct CopyProcedure { + /// The name of the new procedure. + pub name: String, + /// The id of the procedure to copy. + pub id: String, +} + +#[typeshare] +pub type CopyProcedureResponse = Procedure; + +// + +/// Deletes the procedure at the given id, and returns the deleted procedure. +/// Response: [Procedure] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(DeleteProcedureResponse)] +pub struct DeleteProcedure { + /// The id of the procedure to delete. + pub id: String, +} + +#[typeshare] +pub type DeleteProcedureResponse = Procedure; + +// + +/// Update the procedure at the given id, and return the updated procedure. +/// Response: [Procedure]. +/// +/// Note. This method updates only the fields which are set in the [_PartialProcedureConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateProcedureResponse)] +pub struct UpdateProcedure { + pub id: String, + pub config: _PartialProcedureConfig, +} + +#[typeshare] +pub type UpdateProcedureResponse = Procedure; diff --git a/client/core/rs/src/api/write/repo.rs b/client/core/rs/src/api/write/repo.rs new file mode 100644 index 000000000..0dad20617 --- /dev/null +++ b/client/core/rs/src/api/write/repo.rs @@ -0,0 +1,82 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::repo::{Repo, _PartialRepoConfig}; + +use super::MonitorWriteRequest; + +// + +/// Create a repo. Response: [Repo]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Repo)] +pub struct CreateRepo { + /// The name given to newly created repo. + pub name: String, + /// Optional partial config to initialize the repo with. + pub config: _PartialRepoConfig, +} + +// + +/// Creates a new repo with given `name` and the configuration +/// of the repo at the given `id`. Response: [Repo]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Repo)] +pub struct CopyRepo { + /// The name of the new repo. + pub name: String, + /// The id of the repo to copy. + pub id: String, +} + +// + +/// Deletes the repo at the given id, and returns the deleted repo. +/// Response: [Repo] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Repo)] +pub struct DeleteRepo { + /// The id of the repo to delete. + pub id: String, +} + +// + +/// Update the repo at the given id, and return the updated repo. +/// Response: [Repo]. +/// +/// Note. If the attached server for the repo changes, +/// the repo will be deleted / cleaned up on the old server. +/// +/// Note. This method updates only the fields which are set in the [_PartialRepoConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Repo)] +pub struct UpdateRepo { + /// The id of the repo to update. + pub id: String, + /// The partial config update to apply. + pub config: _PartialRepoConfig, +} diff --git a/client/core/rs/src/api/write/server.rs b/client/core/rs/src/api/write/server.rs new file mode 100644 index 000000000..7cb734a32 --- /dev/null +++ b/client/core/rs/src/api/write/server.rs @@ -0,0 +1,115 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + server::{Server, _PartialServerConfig}, + update::Update, +}; + +use super::MonitorWriteRequest; + +// + +/// Create a server. Response: [Server]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Server)] +pub struct CreateServer { + /// The name given to newly created server. + pub name: String, + /// Optional partial config to initialize the server with. + pub config: _PartialServerConfig, +} + +// + +/// Deletes the server at the given id, and returns the deleted server. +/// Response: [Server] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Server)] +pub struct DeleteServer { + /// The id of the server to delete. + pub id: String, +} + +// + +/// Update the server at the given id, and return the updated server. +/// Response: [Server]. +/// +/// Note. This method updates only the fields which are set in the [_PartialServerConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Server)] +pub struct UpdateServer { + /// The id of the server to update. + pub id: String, + /// The partial config update to apply. + pub config: _PartialServerConfig, +} + +// + +/// Rename the server at id to the given name. Response: [Update]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Update)] +pub struct RenameServer { + /// The id of the server to rename. + pub id: String, + /// The new name. + pub name: String, +} + +// + +/// Create a docker network on the server. +/// Respone: [Update] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Update)] +pub struct CreateNetwork { + /// Id or name + pub server: String, + /// The name of the network to create. + pub name: String, +} + +// + +/// Delete a docker network. +/// Response: [Update] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Update)] +pub struct DeleteNetwork { + /// Id or name. + pub server: String, + /// The name of the network to delete. + pub name: String, +} diff --git a/client/core/rs/src/api/write/server_template.rs b/client/core/rs/src/api/write/server_template.rs new file mode 100644 index 000000000..72ab7f086 --- /dev/null +++ b/client/core/rs/src/api/write/server_template.rs @@ -0,0 +1,79 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::server_template::{PartialServerTemplateConfig, ServerTemplate}; + +use super::MonitorWriteRequest; + +// + +/// Create a server template. Response: [ServerTemplate]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(ServerTemplate)] +pub struct CreateServerTemplate { + /// The name given to newly created server template. + pub name: String, + /// Optional partial config to initialize the server template with. + pub config: PartialServerTemplateConfig, +} + +// + +/// Creates a new server template with given `name` and the configuration +/// of the server template at the given `id`. Response: [ServerTemplate] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(ServerTemplate)] +pub struct CopyServerTemplate { + /// The name of the new server template. + pub name: String, + /// The id of the server template to copy. + pub id: String, +} + +// + +/// Deletes the server template at the given id, and returns the deleted server template. +/// Response: [ServerTemplate] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(ServerTemplate)] +pub struct DeleteServerTemplate { + /// The id of the server template to delete. + pub id: String, +} + +// + +/// Update the server template at the given id, and return the updated server template. +/// Response: [ServerTemplate]. +/// +/// Note. This method updates only the fields which are set in the [PartialServerTemplateConfig], +/// effectively merging diffs into the final document. +/// This is helpful when multiple users are using +/// the same resources concurrently by ensuring no unintentional +/// field changes occur from out of date local state. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(ServerTemplate)] +pub struct UpdateServerTemplate { + /// The id of the server template to update. + pub id: String, + /// The partial config update to apply. + pub config: PartialServerTemplateConfig, +} diff --git a/client/core/rs/src/api/write/tags.rs b/client/core/rs/src/api/write/tags.rs new file mode 100644 index 000000000..3f7d0cade --- /dev/null +++ b/client/core/rs/src/api/write/tags.rs @@ -0,0 +1,75 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{tag::Tag, update::ResourceTarget, NoData}; + +use super::MonitorWriteRequest; + +// + +/// Create a tag. Response: [Tag]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Tag)] +pub struct CreateTag { + /// The name of the tag. + pub name: String, +} + +// + +/// Delete a tag, and return the deleted tag. Response: [Tag]. +/// +/// Note. Will also remove this tag from all attached resources. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Tag)] +pub struct DeleteTag { + /// The id of the tag to delete. + pub id: String, +} + +// + +/// Rename a tag at id. Response: [Tag]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(Tag)] +pub struct RenameTag { + /// The id of the tag to rename. + pub id: String, + /// The new name of the tag. + pub name: String, +} + +// + +/// Update the tags on a resource. +/// Response: [NoData] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateTagsOnResourceResponse)] +pub struct UpdateTagsOnResource { + pub target: ResourceTarget, + /// Tag Ids + pub tags: Vec, +} + +#[typeshare] +pub type UpdateTagsOnResourceResponse = NoData; + +// diff --git a/client/core/rs/src/api/write/user.rs b/client/core/rs/src/api/write/user.rs new file mode 100644 index 000000000..c3203c84c --- /dev/null +++ b/client/core/rs/src/api/write/user.rs @@ -0,0 +1,82 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{update::ResourceTarget, user::User, NoData}; + +use super::MonitorWriteRequest; + +// + +/// Push a resource to the front of the users 10 most recently viewed resources. +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(PushRecentlyViewedResponse)] +pub struct PushRecentlyViewed { + /// The target to push. + pub resource: ResourceTarget, +} + +#[typeshare] +pub type PushRecentlyViewedResponse = NoData; + +// + +/// Set the time the user last opened the UI updates. +/// Used for unseen notification dot. +/// Response: [NoData] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(SetLastSeenUpdateResponse)] +pub struct SetLastSeenUpdate {} + +#[typeshare] +pub type SetLastSeenUpdateResponse = NoData; + +// + +/// **Admin only.** Create a service user. +/// Response: [User]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CreateServiceUserResponse)] +pub struct CreateServiceUser { + /// The username for the service user. + pub username: String, + /// A description for the service user. + pub description: String, +} + +#[typeshare] +pub type CreateServiceUserResponse = User; + +// + +/// **Admin only.** Update a service user's description. +/// Response: [User]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateServiceUserDescriptionResponse)] +pub struct UpdateServiceUserDescription { + /// The service user's username + pub username: String, + /// A new description for the service user. + pub description: String, +} + +#[typeshare] +pub type UpdateServiceUserDescriptionResponse = User; diff --git a/client/core/rs/src/api/write/user_group.rs b/client/core/rs/src/api/write/user_group.rs new file mode 100644 index 000000000..39a28821a --- /dev/null +++ b/client/core/rs/src/api/write/user_group.rs @@ -0,0 +1,99 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::user_group::UserGroup; + +use super::MonitorWriteRequest; + +/// **Admin only.** Create a user group. Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct CreateUserGroup { + /// The name to assign to the new UserGroup + pub name: String, +} + +// + +/// **Admin only.** Rename a user group. Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct RenameUserGroup { + /// The id of the UserGroup + pub id: String, + /// The new name for the UserGroup + pub name: String, +} + +// + +/// **Admin only.** Delete a user group. Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct DeleteUserGroup { + /// The id of the UserGroup + pub id: String, +} + +// + +/// **Admin only.** Add a user to a user group. Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct AddUserToUserGroup { + /// The name or id of UserGroup that user should be added to. + pub user_group: String, + /// The id or username of the user to add + pub user: String, +} + +// + +/// **Admin only.** Remove a user from a user group. Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct RemoveUserFromUserGroup { + /// The name or id of UserGroup that user should be removed from. + pub user_group: String, + /// The id or username of the user to remove + pub user: String, +} + +// + +/// **Admin only.** Completely override the user in the group. +/// Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct SetUsersInUserGroup { + /// Id or name. + pub user_group: String, + /// The user ids or usernames to hard set as the group's users. + pub users: Vec, +} diff --git a/client/core/rs/src/busy.rs b/client/core/rs/src/busy.rs new file mode 100644 index 000000000..810908876 --- /dev/null +++ b/client/core/rs/src/busy.rs @@ -0,0 +1,46 @@ +use crate::entities::{ + build::BuildActionState, deployment::DeploymentActionState, + procedure::ProcedureActionState, repo::RepoActionState, + server::ServerActionState, +}; + +pub trait Busy { + fn busy(&self) -> bool; +} + +impl Busy for ServerActionState { + fn busy(&self) -> bool { + self.pruning_containers + || self.pruning_images + || self.pruning_networks + } +} + +impl Busy for DeploymentActionState { + fn busy(&self) -> bool { + self.deploying + || self.removing + || self.starting + || self.stopping + || self.renaming + || self.deleting + } +} + +impl Busy for BuildActionState { + fn busy(&self) -> bool { + self.building + } +} + +impl Busy for RepoActionState { + fn busy(&self) -> bool { + self.cloning || self.pulling || self.updating || self.deleting + } +} + +impl Busy for ProcedureActionState { + fn busy(&self) -> bool { + self.running + } +} diff --git a/client/core/rs/src/entities/alert.rs b/client/core/rs/src/entities/alert.rs new file mode 100644 index 000000000..7fdb31712 --- /dev/null +++ b/client/core/rs/src/entities/alert.rs @@ -0,0 +1,167 @@ +use std::path::PathBuf; + +use derive_variants::EnumVariants; +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + doc, serde_helpers::hex_string_as_object_id, Document, +}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{MongoId, I64}; + +use super::{ + _Serror, deployment::DockerContainerState, + server::stats::SeverityLevel, update::ResourceTarget, +}; + +/// Representation of an alert in the system. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +#[doc_index(doc! { "data.type": 1 })] +#[doc_index(doc! { "target.type": 1 })] +#[doc_index(doc! { "target.id": 1 })] +pub struct Alert { + /// The Mongo ID of the alert. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized Alert) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: MongoId, + + /// Unix timestamp in milliseconds the alert was opened + #[index] + pub ts: I64, + + /// Whether the alert is already resolved + #[index] + pub resolved: bool, + + /// The severity of the alert + #[index] + pub level: SeverityLevel, + + /// The target of the alert + pub target: ResourceTarget, + + /// The type of alert, eg ServerUnreachable, ServerMem, ContainerStateChange + pub variant: AlertDataVariant, + + /// The data attached to the alert + pub data: AlertData, + + /// The timestamp of alert resolution + pub resolved_ts: Option, +} + +/// The variants of data related to the alert. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash +)] +#[serde(tag = "type", content = "data")] +pub enum AlertData { + /// A server could not be reached. + ServerUnreachable { + /// The id of the server + id: String, + /// The name of the server + name: String, + /// The region of the server + region: Option, + /// The error data + err: Option<_Serror>, + }, + + /// A server has high CPU usage. + ServerCpu { + /// The id of the server + id: String, + /// The name of the server + name: String, + /// The region of the server + region: Option, + /// The cpu usage percentage + percentage: f64, + }, + + /// A server has high memory usage. + ServerMem { + /// The id of the server + id: String, + /// The name of the server + name: String, + /// The region of the server + region: Option, + /// The used memory + used_gb: f64, + /// The total memory + total_gb: f64, + }, + + /// A server has high disk usage. + ServerDisk { + /// The id of the server + id: String, + /// The name of the server + name: String, + /// The region of the server + region: Option, + /// The mount path of the disk + path: PathBuf, + /// The used portion of the disk in GB + used_gb: f64, + /// The total size of the disk in GB + total_gb: f64, + }, + + /// A container's state has changed unexpectedly. + ContainerStateChange { + /// The id of the deployment + id: String, + /// The name of the deployment + name: String, + /// The server id of server deployment is on + server_id: String, + /// The server name + server_name: String, + /// The previous container state + from: DockerContainerState, + /// The current container state + to: DockerContainerState, + }, + + /// An AWS builder failed to terminate. + AwsBuilderTerminationFailed { + /// The id of the aws instance which failed to terminate + instance_id: String, + }, + None {}, +} + +impl Default for AlertData { + fn default() -> Self { + AlertData::None {} + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AlertDataVariant { + fn default() -> Self { + AlertDataVariant::None + } +} diff --git a/client/core/rs/src/entities/alerter.rs b/client/core/rs/src/entities/alerter.rs new file mode 100644 index 000000000..dc5cb1544 --- /dev/null +++ b/client/core/rs/src/entities/alerter.rs @@ -0,0 +1,219 @@ +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use derive_variants::EnumVariants; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::{MaybeNone, Partial, PartialDiff}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString}; +use typeshare::typeshare; + +use super::resource::{ + AddFilters, Resource, ResourceListItem, ResourceQuery, +}; + +#[typeshare] +pub type Alerter = Resource; + +#[typeshare] +pub type AlerterListItem = ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AlerterListItemInfo { + /// Whether alerter is enabled for sending alerts + pub enabled: bool, + /// Whether the alerter is the default + pub is_default: bool, + /// The type of the alerter, eg. Slack, Custom + pub alerter_type: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct AlerterInfo { + #[serde(default)] + pub is_default: bool, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString, + AsRefStr +)] +#[serde(tag = "type", content = "params")] +pub enum AlerterConfig { + /// Send alert serialized to JSON to an http endpoint. + Custom(CustomAlerterConfig), + + /// Send alert to a slack app + Slack(SlackAlerterConfig), +} + +impl PartialDiff for AlerterConfig { + fn partial_diff( + &self, + partial: PartialAlerterConfig, + ) -> PartialAlerterConfig { + match self { + AlerterConfig::Custom(original) => match partial { + PartialAlerterConfig::Custom(partial) => { + PartialAlerterConfig::Custom(original.partial_diff(partial)) + } + slack => slack, + }, + AlerterConfig::Slack(original) => match partial { + PartialAlerterConfig::Slack(partial) => { + PartialAlerterConfig::Slack(original.partial_diff(partial)) + } + custom => custom, + }, + } + } +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString, + AsRefStr +)] +#[serde(tag = "type", content = "params")] +pub enum PartialAlerterConfig { + Custom(_PartialCustomAlerterConfig), + Slack(_PartialSlackAlerterConfig), +} + +impl MaybeNone for PartialAlerterConfig { + fn is_none(&self) -> bool { + match self { + PartialAlerterConfig::Custom(config) => config.is_none(), + PartialAlerterConfig::Slack(config) => config.is_none(), + } + } +} + +impl From for AlerterConfig { + fn from(value: PartialAlerterConfig) -> AlerterConfig { + match value { + PartialAlerterConfig::Custom(config) => { + AlerterConfig::Custom(config.into()) + } + PartialAlerterConfig::Slack(config) => { + AlerterConfig::Slack(config.into()) + } + } + } +} + +impl From for PartialAlerterConfig { + fn from(value: AlerterConfig) -> Self { + match value { + AlerterConfig::Custom(config) => { + PartialAlerterConfig::Custom(config.into()) + } + AlerterConfig::Slack(config) => { + PartialAlerterConfig::Slack(config.into()) + } + } + } +} + +impl AlerterConfig { + pub fn merge_partial( + self, + partial: PartialAlerterConfig, + ) -> AlerterConfig { + match partial { + PartialAlerterConfig::Custom(partial) => match self { + AlerterConfig::Custom(config) => { + let config = CustomAlerterConfig { + url: partial.url.unwrap_or(config.url), + enabled: partial.enabled.unwrap_or(config.enabled), + }; + AlerterConfig::Custom(config) + } + _ => AlerterConfig::Custom(partial.into()), + }, + PartialAlerterConfig::Slack(partial) => match self { + AlerterConfig::Slack(config) => { + let config = SlackAlerterConfig { + url: partial.url.unwrap_or(config.url), + enabled: partial.enabled.unwrap_or(config.enabled), + }; + AlerterConfig::Slack(config) + } + _ => AlerterConfig::Slack(partial.into()), + }, + } + } +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialCustomAlerterConfig = PartialCustomAlerterConfig; + +/// Configuration for a custom alerter. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct CustomAlerterConfig { + /// The http/s endpoint to send the POST to + #[partial_default(String::from("http://localhost:7000"))] + pub url: String, + /// Whether the alerter is enabled + #[serde(default)] + pub enabled: bool, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialSlackAlerterConfig = PartialSlackAlerterConfig; + +/// Configuration for a slack alerter. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct SlackAlerterConfig { + /// The slack app url + #[partial_default(String::from( + "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX", + ))] + pub url: String, + /// Whether the alerter is enabled + #[serde(default)] + pub enabled: bool, +} + +#[typeshare] +pub type AlerterQuery = ResourceQuery; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, +)] +pub struct AlerterQuerySpecifics { + pub types: Vec, +} + +impl AddFilters for AlerterQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + let types = + self.types.iter().map(|t| t.as_ref()).collect::>(); + if !self.types.is_empty() { + filters.insert("config.type", doc! { "$in": types }); + } + } +} diff --git a/client/core/rs/src/entities/api_key.rs b/client/core/rs/src/entities/api_key.rs new file mode 100644 index 000000000..e32e26214 --- /dev/null +++ b/client/core/rs/src/entities/api_key.rs @@ -0,0 +1,39 @@ +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::Document; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use super::I64; + +/// An api key used to authenticate requests via request headers. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +pub struct ApiKey { + /// Unique key associated with secret + #[unique_index] + pub key: String, + + /// Hash of the secret + pub secret: String, + + /// User associated with the api key + #[index] + pub user_id: String, + + /// Name associated with the api key for management + pub name: String, + + /// Timestamp of key creation + pub created_at: I64, + + /// Expiry of key, or 0 if never expires + pub expires: I64, +} + +impl ApiKey { + pub fn sanitize(&mut self) { + self.secret.clear() + } +} diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs new file mode 100644 index 000000000..a9e28260c --- /dev/null +++ b/client/core/rs/src/entities/build.rs @@ -0,0 +1,195 @@ +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::I64; + +use super::{ + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + EnvironmentVar, SystemCommand, Version, +}; + +#[typeshare] +pub type Build = Resource; + +#[typeshare] +pub type BuildListItem = ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BuildListItemInfo { + /// Unix timestamp in milliseconds of last build + pub last_built_at: I64, + /// The current version of the build + pub version: Version, + /// The Github repo used as the source of the build + pub repo: String, + /// The branch of the repo + pub branch: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildInfo { + pub last_built_at: I64, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialBuildConfig = PartialBuildConfig; + +/// The build configuration. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct BuildConfig { + /// Which builder is used to build the image. + #[serde(default, alias = "builder")] + #[partial_attr(serde(alias = "builder"))] + #[builder(default)] + pub builder_id: String, + + /// Whether to skip secret interpolation in the build_args. + #[serde(default)] + #[builder(default)] + pub skip_secret_interp: bool, + + /// The current version of the build. + #[serde(default)] + #[builder(default)] + pub version: Version, + + /// The Github repo used as the source of the build. + #[serde(default)] + #[builder(default)] + pub repo: String, + + /// The branch of the repo. + #[serde(default = "default_branch")] + #[builder(default = "default_branch()")] + #[partial_default(default_branch())] + pub branch: String, + + /// The github account used to clone (used to access private repos). + /// Empty string is public clone (only public repos). + #[serde(default)] + #[builder(default)] + pub github_account: String, + + /// The dockerhub account used to push the image to dockerhub. + /// Empty string means no dockerhub push (server local build). + #[serde(default)] + #[builder(default)] + pub docker_account: String, + + /// The docker organization which the image should be pushed under. + /// Empty string means no organization. + #[serde(default)] + #[builder(default)] + pub docker_organization: String, + + /// The optional command run after repo clone and before docker build. + #[serde(default)] + #[builder(default)] + pub pre_build: SystemCommand, + + /// The path of the docker build context relative to the root of the repo. + /// Default: "." (the root of the repo). + #[serde(default = "default_build_path")] + #[builder(default = "default_build_path()")] + #[partial_default(default_build_path())] + pub build_path: String, + + /// The path of the dockerfile relative to the build path. + #[serde(default = "default_dockerfile_path")] + #[builder(default = "default_dockerfile_path()")] + #[partial_default(default_dockerfile_path())] + pub dockerfile_path: String, + + /// Docker build arguments + #[serde(default)] + #[builder(default)] + pub build_args: Vec, + + /// Docker labels + #[serde(default)] + #[builder(default)] + pub labels: Vec, + + /// Any extra docker cli arguments to be included in the build command + #[serde(default)] + #[builder(default)] + pub extra_args: Vec, + + /// Whether to use buildx to build (eg `docker buildx build ...`) + #[serde(default)] + #[builder(default)] + pub use_buildx: bool, +} + +impl BuildConfig { + pub fn builder() -> BuildConfigBuilder { + BuildConfigBuilder::default() + } +} + +fn default_branch() -> String { + String::from("main") +} + +fn default_build_path() -> String { + String::from(".") +} + +fn default_dockerfile_path() -> String { + String::from("Dockerfile") +} + +#[typeshare] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct BuildActionState { + pub building: bool, +} + +#[typeshare] +pub type BuildQuery = ResourceQuery; + +#[typeshare] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, +)] +pub struct BuildQuerySpecifics { + #[serde(default)] + pub builder_ids: Vec, + + #[serde(default)] + pub repos: Vec, + + /// query for builds last built more recently than this timestamp + /// defaults to 0 which is a no op + #[serde(default)] + pub built_since: I64, +} + +impl AddFilters for BuildQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + if !self.builder_ids.is_empty() { + filters.insert( + "config.builder_id", + doc! { "$in": &self.builder_ids }, + ); + } + if !self.repos.is_empty() { + filters.insert("config.repo", doc! { "$in": &self.repos }); + } + if self.built_since > 0 { + filters.insert( + "info.last_built_at", + doc! { "$gte": self.built_since }, + ); + } + } +} diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs new file mode 100644 index 000000000..0cf040d82 --- /dev/null +++ b/client/core/rs/src/entities/builder.rs @@ -0,0 +1,280 @@ +use derive_builder::Builder; +use derive_variants::EnumVariants; +use partial_derive2::{MaybeNone, Partial, PartialDiff}; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; +use typeshare::typeshare; + +use super::resource::{ + AddFilters, Resource, ResourceListItem, ResourceQuery, +}; + +#[typeshare] +pub type Builder = Resource; + +#[typeshare] +pub type BuilderListItem = ResourceListItem; + +#[typeshare(serialized_as = "Partial")] +pub type _PartialBuilderConfig = PartialBuilderConfig; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BuilderListItemInfo { + pub builder_type: String, + pub instance_type: Option, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString +)] +#[serde(tag = "type", content = "params")] +pub enum BuilderConfig { + /// Use a connected server an image builder. + Server(ServerBuilderConfig), + + /// Use EC2 instances spawned on demand as an image builder. + Aws(AwsBuilderConfig), +} + +impl PartialDiff for BuilderConfig { + fn partial_diff( + &self, + partial: PartialBuilderConfig, + ) -> PartialBuilderConfig { + match self { + BuilderConfig::Server(original) => match partial { + PartialBuilderConfig::Server(partial) => { + PartialBuilderConfig::Server(original.partial_diff(partial)) + } + aws => aws, + }, + BuilderConfig::Aws(original) => match partial { + PartialBuilderConfig::Aws(partial) => { + PartialBuilderConfig::Aws(original.partial_diff(partial)) + } + server => server, + }, + } + } +} + +/// Partial representation of [BuilderConfig] +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString +)] +#[serde(tag = "type", content = "params")] +pub enum PartialBuilderConfig { + Server(_PartialServerBuilderConfig), + Aws(_PartialAwsBuilderConfig), +} + +impl MaybeNone for PartialBuilderConfig { + fn is_none(&self) -> bool { + match self { + PartialBuilderConfig::Server(config) => config.is_none(), + PartialBuilderConfig::Aws(config) => config.is_none(), + } + } +} + +impl From for BuilderConfig { + fn from(value: PartialBuilderConfig) -> BuilderConfig { + match value { + PartialBuilderConfig::Server(server) => { + BuilderConfig::Server(server.into()) + } + PartialBuilderConfig::Aws(builder) => { + BuilderConfig::Aws(builder.into()) + } + } + } +} + +impl From for PartialBuilderConfig { + fn from(value: BuilderConfig) -> Self { + match value { + BuilderConfig::Server(config) => { + PartialBuilderConfig::Server(config.into()) + } + BuilderConfig::Aws(config) => { + PartialBuilderConfig::Aws(config.into()) + } + } + } +} + +impl BuilderConfig { + pub fn merge_partial( + self, + partial: PartialBuilderConfig, + ) -> BuilderConfig { + match partial { + PartialBuilderConfig::Server(partial) => match self { + BuilderConfig::Server(config) => { + let config = ServerBuilderConfig { + server_id: partial.server_id.unwrap_or(config.server_id), + }; + BuilderConfig::Server(config) + } + _ => BuilderConfig::Server(partial.into()), + }, + PartialBuilderConfig::Aws(partial) => match self { + BuilderConfig::Aws(config) => { + let config = AwsBuilderConfig { + region: partial.region.unwrap_or(config.region), + instance_type: partial + .instance_type + .unwrap_or(config.instance_type), + volume_gb: partial.volume_gb.unwrap_or(config.volume_gb), + ami_id: partial.ami_id.unwrap_or(config.ami_id), + subnet_id: partial.subnet_id.unwrap_or(config.subnet_id), + security_group_ids: partial + .security_group_ids + .unwrap_or(config.security_group_ids), + key_pair_name: partial + .key_pair_name + .unwrap_or(config.key_pair_name), + assign_public_ip: partial + .assign_public_ip + .unwrap_or(config.assign_public_ip), + use_public_ip: partial + .use_public_ip + .unwrap_or(config.use_public_ip), + port: partial.port.unwrap_or(config.port), + github_accounts: partial + .github_accounts + .unwrap_or(config.github_accounts), + docker_accounts: partial + .docker_accounts + .unwrap_or(config.docker_accounts), + }; + BuilderConfig::Aws(config) + } + _ => BuilderConfig::Aws(partial.into()), + }, + } + } +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialServerBuilderConfig = PartialServerBuilderConfig; + +/// Configuration for a monitor server builder. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct ServerBuilderConfig { + /// The server id of the builder + #[serde(alias = "server")] + #[partial_attr(serde(alias = "server"))] + pub server_id: String, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialAwsBuilderConfig = PartialAwsBuilderConfig; + +/// Configuration for an AWS builder. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct AwsBuilderConfig { + /// The AWS region to create the instance in + #[serde(default = "aws_default_region")] + #[builder(default = "aws_default_region()")] + #[partial_default(aws_default_region())] + pub region: String, + + /// The instance type to create for the build + #[serde(default = "aws_default_instance_type")] + #[builder(default = "aws_default_instance_type()")] + #[partial_default(aws_default_instance_type())] + pub instance_type: String, + + /// The size of the builder volume in gb + #[serde(default = "aws_default_volume_gb")] + #[builder(default = "aws_default_volume_gb()")] + #[partial_default(aws_default_volume_gb())] + pub volume_gb: i32, + + /// The port periphery will be running on. + /// Default: `8120` + #[serde(default = "default_port")] + #[builder(default = "default_port()")] + #[partial_default(default_port())] + pub port: i32, + + /// The EC2 ami id to create. + /// The ami should have the periphery client configured to start on startup, + /// and should have the necessary github / dockerhub accounts configured. + pub ami_id: String, + /// The subnet id to create the instance in. + pub subnet_id: String, + /// The security group ids to attach to the instance. + /// This should include a security group to allow core inbound access to the periphery port. + pub security_group_ids: Vec, + /// The key pair name to attach to the instance + pub key_pair_name: String, + /// Whether to assign the instance a public IP address. + /// Likely needed for the instance to be able to reach the open internet. + pub assign_public_ip: bool, + /// Whether core should use the public IP address to communicate with periphery on the builder. + /// If false, core will communicate with the instance using the private IP. + pub use_public_ip: bool, + + /// Which github accounts (usernames) are available on the AMI + #[serde(default)] + pub github_accounts: Vec, + /// Which dockerhub accounts (usernames) are available on the AMI + #[serde(default)] + pub docker_accounts: Vec, +} + +impl AwsBuilderConfig { + pub fn builder() -> AwsBuilderConfigBuilder { + AwsBuilderConfigBuilder::default() + } +} + +fn aws_default_region() -> String { + String::from("us-east-1") +} + +fn aws_default_instance_type() -> String { + String::from("c5.2xlarge") +} + +fn aws_default_volume_gb() -> i32 { + 20 +} + +fn default_port() -> i32 { + 8120 +} + +#[typeshare] +pub type BuilderQuery = ResourceQuery; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuilderQuerySpecifics {} + +impl AddFilters for BuilderQuerySpecifics {} diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs new file mode 100644 index 000000000..892e2e99b --- /dev/null +++ b/client/core/rs/src/entities/config/core.rs @@ -0,0 +1,452 @@ +//! # Configuring the Core API +//! +//! Monitor core is configured by parsing base configuration file ([CoreConfig]), and overriding +//! any fields given in the file with ones provided on the environment ([Env]). +//! +//! The recommended method for running monitor core is via the docker image. This image has a default +//! configuration file provided in the image, meaning any custom configuration can be provided +//! on the environment alone. However, if a custom configuration file is prefered, it can be mounted +//! into the image at `/config/config.toml`. +//! + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::entities::{ + logger::{LogConfig, LogLevel, StdioLogMode}, + Timelength, +}; + +/// # Monitor Core Environment Variables +/// +/// You can override any fields of the [CoreConfig] by passing the associated +/// environment variable. The variables should be passed in the traditional `UPPER_SNAKE_CASE` format, +/// although the lower case format can still be parsed. +/// +/// *Note.* The monitor core docker image includes the default core configuration found in +/// the `mbecker20/monitor/config_example` folder of the repo. To configure the core api, +/// you can either mount your own custom configuration file to `/config/config.toml` inside the container, +/// or simply override whichever fields you need using the environment. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Env { + /// Specify a custom config path for the core config toml. + /// Default: `/config/config.toml` + #[serde(default = "default_config_path")] + pub monitor_config_path: String, + + /// Override `title` + pub monitor_title: Option, + /// Override `host` + pub monitor_host: Option, + /// Override `port` + pub monitor_port: Option, + /// Override `passkey` + pub monitor_passkey: Option, + /// Override `jwt_valid_for` + pub monitor_jwt_valid_for: Option, + /// Override `monitoring_interval` + pub monitor_monitoring_interval: Option, + /// Override `keep_stats_for_days` + pub monitor_keep_stats_for_days: Option, + /// Override `keep_alerts_for_days` + pub monitor_keep_alerts_for_days: Option, + /// Override `github_webhook_secret` + pub monitor_github_webhook_secret: Option, + /// Override `github_webhook_base_url` + pub monitor_github_webhook_base_url: Option, + /// Override `docker_organizations` + pub monitor_docker_organizations: Option>, + + /// Override `logging.level` + pub monitor_logging_level: Option, + /// Override `logging.stdio` + pub monitor_logging_stdio: Option, + /// Override `logging.otlp_endpoint` + pub monitor_logging_otlp_endpoint: Option, + /// Override `logging.opentelemetry_service_name` + pub monitor_logging_opentelemetry_service_name: Option, + + /// Override `local_auth` + pub monitor_local_auth: Option, + + /// Override `github_oauth.enabled` + pub monitor_github_oauth_enabled: Option, + /// Override `github_oauth.id` + pub monitor_github_oauth_id: Option, + /// Override `github_oauth.secret` + pub monitor_github_oauth_secret: Option, + + /// Override `google_oauth.enabled` + pub monitor_google_oauth_enabled: Option, + /// Override `google_oauth.id` + pub monitor_google_oauth_id: Option, + /// Override `google_oauth.secret` + pub monitor_google_oauth_secret: Option, + + /// Override `mongo.uri` + pub monitor_mongo_uri: Option, + /// Override `mongo.address` + pub monitor_mongo_address: Option, + /// Override `mongo.username` + pub monitor_mongo_username: Option, + /// Override `mongo.password` + pub monitor_mongo_password: Option, + /// Override `mongo.app_name` + pub monitor_mongo_app_name: Option, + /// Override `mongo.db_name` + pub monitor_mongo_db_name: Option, + + /// Override `aws.access_key_id` + pub monitor_aws_access_key_id: Option, + /// Override `aws.secret_access_key` + pub monitor_aws_secret_access_key: Option, +} + +fn default_config_path() -> String { + "/config/config.toml".to_string() +} + +/// # Core Configuration File +/// +/// The Core API initializes it's configuration by reading the environment, +/// parsing the [CoreConfig] schema from the file path specified by `env.monitor_config_path`, +/// and then applying any config field overrides specified in the environment. +/// +/// *Note.* The monitor core docker image includes the default core configuration found below. +/// To configure the core api, you can either mount your own custom configuration file +/// to `/config/config.toml` inside the container, or simply override whichever fields +/// you need using the environment. +/// +/// ## Example TOML +/// ```toml +/// ## this will be the document title on the web page (shows up as text in the browser tab). +/// ## default: 'Monitor' +/// title = "Monitor" +/// +/// ## required for oauth functionality. this should be the url used to access monitor in browser, +/// ## potentially behind DNS. +/// ## eg https://monitor.dev or http://12.34.56.78:9000. +/// ## this should match the address configured in your oauth app. +/// ## no default +/// host = "https://monitor.dev" +/// +/// ## the port the core system will run on. if running core in docker container, +/// ## leave as this port as 9000 and use port bind eg. -p 9001:9000 +/// ## default: 9000 +/// port = 9000 +/// +/// ## required to match a passkey in periphery config. +/// ## token used to authenticate core requests to periphery +/// ## no default +/// passkey = "a_random_passkey" +/// +/// ## specify the log level of the monitor core application +/// ## default: info +/// ## options: off, error, warn, info, debug, trace +/// logging.level = "info" +/// +/// ## specify the logging format for stdout / stderr. +/// ## default: standard +/// ## options: standard, json, none +/// logging.stdio = "standard" +/// +/// ## specify a opentelemetry otlp endpoint to send traces to +/// ## optional, default unassigned +/// # logging.otlp_endpoint = "http://localhost:4317" +/// +/// ## specify how long an issued jwt stays valid. +/// ## all jwts are invalidated on application restart. +/// ## default: 1-day. +/// ## options: 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk, 30-day +/// jwt_valid_for = "1-day" +/// +/// ## controls the granularity of the system stats collection by monitor core +/// ## default: 15-sec +/// ## options: 5-sec, 15-sec, 30-sec, 1-min, 2-min, 5-min, 15-min +/// monitoring_interval = "15-sec" +/// +/// ## number of days to keep stats around, or 0 to disable pruning. +/// ## stats older than this number of days are deleted daily +/// ## default: 0 (pruning disabled) +/// keep_stats_for_days = 0 +/// +/// ## token that has to be given to github during repo webhook config as the secret +/// ## default: empty (none) +/// github_webhook_secret = "your_random_webhook_secret" +/// +/// ## an alternate base url that is used to recieve github webhook requests +/// ## if empty or not specified, will use 'host' address as base +/// ## default: empty (none) +/// # github_webhook_base_url = "https://github-webhook.monitor.dev" +/// +/// ## these will be used by the GUI to attach to builds. +/// ## when attached to build, image will be pushed to repo under the specified organization. +/// ## if empty, the "docker organization" config option will not be shown. +/// ## default: empty +/// # docker_organizations = ["your_docker_org1", "your_docker_org_2"] +/// +/// ## allow or deny user login with username / password +/// ## default: false +/// # local_auth = true +/// +/// ## Use to configure github oauth +/// # github_oauth.enabled = true +/// # github_oauth.id = "your_github_client_id" +/// # github_oauth.secret = "your_github_client_secret" +/// +/// ## Use to configure google oauth +/// # google_oauth.enabled = true +/// # google_oauth.id = "your_google_client_id" +/// # google_oauth.secret = "your_google_client_secret" +/// +/// ## MUST comment back in some way to configure mongo. +/// # mongo.uri = "mongodb://username:password@localhost:27017" +/// ## ==== or ==== +/// mongo.address = "localhost:27017" +/// # mongo.username = "username" +/// # mongo.password = "password" +/// ## ==== other ==== +/// ## default: monitor. this is the name of the mongo database that monitor will create its collections in. +/// mongo.db_name = "monitor" +/// ## default: monitor_core. this is the assigned app_name of the mongo client +/// mongo.app_name = "monitor_core" +/// +/// ## provide aws api keys for ephemeral builders +/// # aws.access_key_id = "your_aws_key_id" +/// # aws.secret_access_key = "your_aws_secret_key" +/// +/// ## provide core-based github accounts +/// [github_accounts] +/// # github_username_1 = "github_token_1" +/// # github_username_2 = "github_token_2" +/// +/// ## provide core-based docker accounts +/// [docker_accounts] +/// # docker_username_1 = "docker_token_1" +/// # docker_username_2 = "docker_token_2" +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CoreConfig { + /// The title of this monitor deployment. Will be used in the browser page title. + /// Default: 'Monitor' + #[serde(default = "default_title")] + pub title: String, + + /// The host to use with oauth redirect url, whatever host + /// the user hits to access monitor. eg `https://monitor.mogh.tech`. + /// Only used if oauth used without user specifying redirect url themselves. + #[serde(default)] + pub host: String, + + /// Port the core web server runs on. + /// Default: 9000. + #[serde(default = "default_core_port")] + pub port: u16, + + /// Sent in auth header with req to periphery. + /// Should be some secure hash, maybe 20-40 chars. + pub passkey: String, + + /// Control how long distributed JWT remain valid for. + /// Default: `1-day`. + #[serde(default = "default_jwt_valid_for")] + pub jwt_valid_for: Timelength, + + /// Interval at which to collect server stats and send any alerts. + /// Default: `15-sec` + #[serde(default = "default_monitoring_interval")] + pub monitoring_interval: Timelength, + + /// Number of days to keep stats, or 0 to disable pruning. stats older than this number of days are deleted on a daily cycle + /// Default: 0 (no pruning). + #[serde(default)] + pub keep_stats_for_days: u64, + + /// Number of days to keep alerts, or 0 to disable pruning. alerts older than this number of days are deleted on a daily cycle + /// Default: 0 (no pruning). + #[serde(default)] + pub keep_alerts_for_days: u64, + + /// Used to verify validity from github webhooks. + /// Should be some secure hash maybe 20-40 chars. + /// It needs to be given to github when configuring the webhook. + #[serde(default)] + pub github_webhook_secret: String, + + /// Used to form the frontend listener url, if None will use 'host'. + /// + /// This can be used if core sits on an internal network which is + /// unreachable directly from the open internet. + /// A reverse proxy in a public network (with its own DNS) + /// can forward webhooks to the internal monitor + pub github_webhook_base_url: Option, + + /// allowed docker orgs used with monitor. first in this list will be default for build. + /// Default: none. + #[serde(default)] + pub docker_organizations: Vec, + + /// Configure logging + #[serde(default)] + pub logging: LogConfig, + + /// enable login with local auth + #[serde(default)] + pub local_auth: bool, + + /// Configure github oauth + #[serde(default)] + pub github_oauth: OauthCredentials, + + /// Configure google oauth + #[serde(default)] + pub google_oauth: OauthCredentials, + + /// Configure core mongo connection. + /// + /// An easy deployment method is to use Mongo Atlas to provide + /// a reliable database. + pub mongo: MongoConfig, + + /// Configure AWS credentials to use with AWS builds / server launches. + #[serde(default)] + pub aws: AwsCredentials, + + /// Configure core-based github accounts. These will be preferentially attached to build / repo clone + /// requests if they contain a matching github account. Otherwise, the periphery will have to have the + /// account configured. + #[serde(default)] + pub github_accounts: HashMap, + + /// Configure core-based docker accounts. These will be preferentially attached to build / deploy + /// requests if they contain a matching docker account. Otherwise, the periphery will have to have the + /// account configured. + #[serde(default)] + pub docker_accounts: HashMap, +} + +fn default_title() -> String { + String::from("Monitor") +} + +fn default_core_port() -> u16 { + 9000 +} + +fn default_jwt_valid_for() -> Timelength { + Timelength::OneDay +} + +fn default_monitoring_interval() -> Timelength { + Timelength::FifteenSeconds +} + +impl CoreConfig { + pub fn sanitized(&self) -> CoreConfig { + let mut config = self.clone(); + + config.passkey = empty_or_redacted(&config.passkey); + config.github_webhook_secret = + empty_or_redacted(&config.github_webhook_secret); + + config.github_oauth.id = + empty_or_redacted(&config.github_oauth.id); + config.github_oauth.secret = + empty_or_redacted(&config.github_oauth.secret); + + config.google_oauth.id = + empty_or_redacted(&config.google_oauth.id); + config.google_oauth.secret = + empty_or_redacted(&config.google_oauth.secret); + + config.mongo.uri = + config.mongo.uri.map(|cur| empty_or_redacted(&cur)); + config.mongo.username = + config.mongo.username.map(|cur| empty_or_redacted(&cur)); + config.mongo.password = + config.mongo.password.map(|cur| empty_or_redacted(&cur)); + + config.aws.access_key_id = + empty_or_redacted(&config.aws.access_key_id); + config.aws.secret_access_key = + empty_or_redacted(&config.aws.secret_access_key); + + config + } +} + +fn empty_or_redacted(src: &str) -> String { + if src.is_empty() { + String::new() + } else { + String::from("##############") + } +} + +/// Generic Oauth credentials +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct OauthCredentials { + /// Whether this oauth method is available for usage. + #[serde(default)] + pub enabled: bool, + /// The Oauth client id. + #[serde(default)] + pub id: String, + /// The Oauth client secret. + #[serde(default)] + pub secret: String, +} + +/// Provide mongo connection information. +/// Must provide ONE of: +/// 1. `uri` +/// 2. `address` + `username` + `password` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MongoConfig { + /// Full mongo uri string, eg. `mongodb://username:password@your.mongo.int:27017` + pub uri: Option, + /// Just the address part of the uri, eg `your.mongo.int:27017` + pub address: Option, + /// Mongo user username + pub username: Option, + /// Mongo user password + pub password: Option, + /// Mongo app name. default: `monitor_core` + #[serde(default = "default_core_mongo_app_name")] + pub app_name: String, + /// Mongo db name. Which mongo database to create the collections in. + /// Default: `monitor`. + #[serde(default = "default_core_mongo_db_name")] + pub db_name: String, +} + +fn default_core_mongo_app_name() -> String { + "monitor_core".to_string() +} + +fn default_core_mongo_db_name() -> String { + "monitor".to_string() +} + +impl Default for MongoConfig { + fn default() -> Self { + Self { + uri: None, + address: Some("localhost:27017".to_string()), + username: None, + password: None, + app_name: default_core_mongo_app_name(), + db_name: default_core_mongo_db_name(), + } + } +} + +/// Provide AWS credentials for monitor to use. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AwsCredentials { + /// The aws ACCESS_KEY_ID + pub access_key_id: String, + /// The aws SECRET_ACCESS_KEY + pub secret_access_key: String, +} diff --git a/client/core/rs/src/entities/config/mod.rs b/client/core/rs/src/entities/config/mod.rs new file mode 100644 index 000000000..95a26b88b --- /dev/null +++ b/client/core/rs/src/entities/config/mod.rs @@ -0,0 +1,2 @@ +pub mod core; +pub mod periphery; diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs new file mode 100644 index 000000000..8f80652e6 --- /dev/null +++ b/client/core/rs/src/entities/config/periphery.rs @@ -0,0 +1,241 @@ +//! # Configuring the periphery agent +//! +//! The periphery configuration is passed in three ways: +//! 1. Command line args ([CliArgs]) +//! 2. Environment Variables ([Env]) +//! 3. Configuration File ([PeripheryConfig]) +//! +//! The final configuration is built by combining parameters +//! passed through the different methods. The priority of the args is +//! strictly hierarchical, meaning params passed with [CliArgs] have top priority, +//! followed by those passed in the environment, followed by those passed in +//! the configuration file. +//! + +use std::{collections::HashMap, net::IpAddr, path::PathBuf}; + +use clap::Parser; +use serde::Deserialize; + +use crate::entities::{ + logger::{LogConfig, LogLevel, StdioLogMode}, + Timelength, +}; + +/// # Periphery Command Line Arguments. +/// +/// This structure represents the periphery command line arguments used to +/// configure the periphery agent. A help manual for the periphery binary +/// can be printed using `/path/to/periphery --help`. +#[derive(Parser)] +#[command(author, about, version)] +pub struct CliArgs { + /// Sets the path of a config file or directory to use. + /// Can use multiple times + #[arg(short, long)] + pub config_path: Option>, + + /// Sets the keywords to match directory periphery config file names on. + /// Can use multiple times. + #[arg(long)] + pub config_keyword: Option>, + + /// Merges nested configs, eg. secrets, docker_accounts, github_accounts. + /// Will override the equivalent env configuration. + #[arg(long)] + pub merge_nested_config: Option, + + /// Extends config arrays, eg. allowed_ips, passkeys. + /// Will override the equivalent env configuration. + #[arg(long)] + pub extend_config_arrays: Option, + + /// Configure the logging level: error, warn, info, debug, trace. + /// If passed, will override any other log_level set. + #[arg(long)] + pub log_level: Option, +} + +/// # Periphery Environment Variables +/// +/// The variables should be passed in the traditional `UPPER_SNAKE_CASE` format, +/// although the lower case format can still be parsed. If equivalent paramater is passed +/// in [CliArgs], the value passed to the environment will be ignored in favor of the cli arg. +#[derive(Deserialize)] +pub struct Env { + /// Specify the config paths (files or folders) used to build up the + /// final [PeripheryConfig]. + /// Default: `~/.config/monitor/periphery.config.toml`. + /// + /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. + #[serde(default = "default_config_paths")] + pub monitor_config_paths: Vec, + /// If specifying folders, use this to narrow down which + /// files will be matched to parse into the final [PeripheryConfig]. + /// Only files inside the folders which have names containing all keywords + /// provided to `config_keywords` will be included. + /// + /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. + #[serde(default)] + pub monitor_config_keywords: Vec, + + /// Will merge nested config object (eg. secrets, github_accounts) across multiple + /// config files. Default: `false` + /// + /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. + #[serde(default)] + pub monitor_merge_nested_config: bool, + + /// Will extend config arrays (eg. `allowed_ips`, `passkeys`) across multiple config files. + /// Default: `false` + /// + /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. + #[serde(default)] + pub monitor_extend_config_arrays: bool, + + /// Override `port` + pub monitor_port: Option, + /// Override `repo_dir` + pub monitor_repo_dir: Option, + /// Override `stats_polling_rate` + pub monitor_stats_polling_rate: Option, + + // LOGGING + /// Override `logging.level` + pub monitor_logging_level: Option, + /// Override `logging.stdio` + pub monitor_logging_stdio: Option, + /// Override `logging.otlp_endpoint` + pub monitor_logging_otlp_endpoint: Option, + /// Override `logging.opentelemetry_service_name` + pub monitor_logging_opentelemetry_service_name: Option, + + /// Override `allowed_ips` + pub monitor_allowed_ips: Option>, + /// Override `passkeys` + pub monitor_passkeys: Option>, +} + +fn default_config_paths() -> Vec { + vec!["~/.config/monitor/periphery.config.toml".to_string()] +} + +/// # Periphery Configuration File +/// +/// The periphery agent initializes it's configuration by reading the environment, +/// parsing the [PeripheryConfig] schema from the files specified by cli args (and falling back to `env.config_paths`), +/// and then applying any config field overrides specified in the environment. +/// +/// ## Example TOML +/// ```toml +/// ## optional. 8120 is default +/// port = 8120 +/// +/// ## optional. /repos is default. +/// repo_dir = "/repos" +/// +/// ## optional. 5-sec is default. +/// ## can use 1-sec, 5-sec, 10-sec, 30-sec, 1-min. +/// ## controls granularity of system stats recorded +/// stats_polling_rate = "5-sec" +/// +/// ## optional. default is empty, which will not block any request by ip. +/// allowed_ips = ["127.0.0.1"] +/// +/// ## optional. default is empty, which will not require any passkey to be passed by core. +/// passkeys = ["abcdefghijk"] +/// +/// ## specify the log level of the monitor core application +/// ## default: info +/// ## options: off, error, warn, info, debug, trace +/// logging.level = "info" +/// +/// ## specify the logging format for stdout / stderr. +/// ## default: standard +/// ## options: standard, json, none +/// logging.stdio = "standard" +/// +/// ## specify an otlp endpoint to send traces to +/// ## optional, default unassigned +/// # logging.otlp_endpoint = "http://localhost:4317" +/// +/// ## specify the service name to send with otlp traces. +/// ## optional, default 'Monitor'. +/// # logging.opentelemetry_service_name = "Monitor" +/// +/// ## optional. can inject these values into your deployments configuration. +/// [secrets] +/// secret_variable = "secret_value" +/// +/// ## optional. can use these accounts with deployments / builds. +/// [github_accounts] +/// github_username1 = "github_token1" +/// github_username2 = "github_token2" +/// +/// ## optional. can use these accounts with deployments / builds. +/// [docker_accounts] +/// docker_username1 = "docker_token1" +/// docker_username2 = "docker_token2" +/// ``` +#[derive(Deserialize, Debug, Clone)] +pub struct PeripheryConfig { + /// The port periphery will run on. + /// Default: `8120` + #[serde(default = "default_periphery_port")] + pub port: u16, + + /// The system directory where monitor managed repos will be cloned. + /// Default: `/repos` + #[serde(default = "default_repo_dir")] + pub repo_dir: PathBuf, + + /// The rate at which the system stats will be polled to update the cache. + /// Default: `5-sec` + #[serde(default = "default_stats_refresh_interval")] + pub stats_polling_rate: Timelength, + + /// Logging configuration + #[serde(default)] + pub logging: LogConfig, + + /// Limits which IPv4 addresses are allowed to call the api. + /// Default: none + /// + /// Note: this should be configured to increase security. + #[serde(default)] + pub allowed_ips: Vec, + + /// Limits the accepted passkeys. + /// Default: none + /// + /// Note: this should be configured to increase security. + #[serde(default)] + pub passkeys: Vec, + + /// Mapping on local periphery secrets. These can be interpolated into eg. Deployment environment variables. + /// Default: none + #[serde(default)] + pub secrets: HashMap, + + /// Mapping of github usernames to access tokens. + /// Default: none + #[serde(default)] + pub github_accounts: HashMap, + + /// Mapping of docker usernames to access tokens. + /// Default: none + #[serde(default)] + pub docker_accounts: HashMap, +} + +fn default_periphery_port() -> u16 { + 8120 +} + +fn default_repo_dir() -> PathBuf { + "/repos".parse().unwrap() +} + +fn default_stats_refresh_interval() -> Timelength { + Timelength::FiveSeconds +} diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs new file mode 100644 index 000000000..aa48e9521 --- /dev/null +++ b/client/core/rs/src/entities/deployment.rs @@ -0,0 +1,409 @@ +use std::collections::HashMap; + +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use derive_variants::EnumVariants; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; +use typeshare::typeshare; + +use super::{ + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + EnvironmentVar, Version, +}; + +#[typeshare] +pub type Deployment = Resource; + +#[typeshare] +pub type DeploymentListItem = + ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DeploymentListItemInfo { + /// The state of the docker container. + pub state: DockerContainerState, + /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) + pub status: Option, + /// The image attached to the deployment. + pub image: String, + /// The server that deployment sits on. + pub server_id: String, + /// An attached monitor build, if it exists. + pub build_id: Option, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialDeploymentConfig = PartialDeploymentConfig; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone)] +#[partial(skip_serializing_none, from, diff)] +pub struct DeploymentConfig { + /// The id of server the deployment is deployed on. + #[serde(default, alias = "server")] + #[partial_attr(serde(alias = "server"))] + #[builder(default)] + pub server_id: String, + + /// Whether to send ContainerStateChange alerts for this deployment. + #[serde(default = "default_send_alerts")] + #[builder(default = "default_send_alerts()")] + #[partial_default(default_send_alerts())] + pub send_alerts: bool, + + /// The image which the deployment deploys. + /// Can either be a user inputted image, or a Monitor build. + #[serde(default)] + #[builder(default)] + pub image: DeploymentImage, + + /// Whether to skip secret interpolation into the deployment environment variables. + #[serde(default)] + #[builder(default)] + pub skip_secret_interp: bool, + + /// Whether to redeploy the deployment whenever the attached build finishes. + #[serde(default)] + #[builder(default)] + pub redeploy_on_build: bool, + + /// Labels attached to various termination signal options. + /// Used to specify different shutdown functionality depending on the termination signal. + #[serde(default = "default_term_signal_labels")] + #[builder(default = "default_term_signal_labels()")] + #[partial_default(default_term_signal_labels())] + pub term_signal_labels: Vec, + + /// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). + #[serde(default)] + #[builder(default)] + pub termination_signal: TerminationSignal, + + /// The termination timeout. + #[serde(default = "default_termination_timeout")] + #[builder(default = "default_termination_timeout()")] + #[partial_default(default_termination_timeout())] + pub termination_timeout: i32, + + /// The container port mapping. + /// Irrelevant if container network is `host`. + /// Maps ports on host to ports on container. + #[serde(default)] + #[builder(default)] + pub ports: Vec, + + /// The container volume mapping. + /// Maps files / folders on host to files / folders in container. + #[serde(default)] + #[builder(default)] + pub volumes: Vec, + + /// The environment variables passed to the container. + #[serde(default)] + #[builder(default)] + pub environment: Vec, + + /// The docker labels given to the container. + #[serde(default)] + #[builder(default)] + pub labels: Vec, + + /// The network attached to the container. + /// Default is `host`. + #[serde(default = "default_network")] + #[builder(default = "default_network()")] + #[partial_default(default_network())] + pub network: String, + + /// The restart mode given to the container. + #[serde(default)] + #[builder(default)] + pub restart: RestartMode, + + /// This is interpolated at the end of the `docker run` command, + /// which means they are either passed to the containers inner process, + /// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile. + /// Empty is no process args. + #[serde(default)] + #[builder(default)] + pub process_args: String, + + /// Extra args which are interpolated into the `docker run` command, + /// and affect the container configuration. + #[serde(default)] + #[builder(default)] + pub extra_args: Vec, + + /// The docker account the deployment should use to pull the image. + /// - If using a custom image, empty string means don't use an account. Only works for public images. + /// - If using a monitor build, empty string means to use the same docker account as the build uses. + #[serde(default)] + #[builder(default)] + pub docker_account: String, +} + +impl DeploymentConfig { + pub fn builder() -> DeploymentConfigBuilder { + DeploymentConfigBuilder::default() + } +} + +fn default_send_alerts() -> bool { + true +} + +fn default_term_signal_labels() -> Vec { + vec![TerminationSignalLabel::default()] +} + +fn default_termination_timeout() -> i32 { + 10 +} + +fn default_network() -> String { + String::from("host") +} + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, EnumVariants, +)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Display, + EnumString +)] +#[serde(tag = "type", content = "params")] +pub enum DeploymentImage { + /// Deploy any external image. + Image { + /// The docker image, can be from any registry that works with docker and that the host server can reach. + #[serde(default)] + image: String, + }, + + /// Deploy a monitor build. + Build { + /// The id of the build + #[serde(default, alias = "build")] + build_id: String, + /// Use a custom / older version of the image produced by the build. + /// if version is 0.0.0, this means `latest` image. + #[serde(default)] + version: Version, + }, +} + +impl Default for DeploymentImage { + fn default() -> Self { + Self::Image { + image: Default::default(), + } + } +} + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct Conversion { + /// reference on the server. + pub local: String, + /// reference in the container. + pub container: String, +} + +/// A summary of a docker container on a server. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ContainerSummary { + /// Name of the container. + pub name: String, + /// Id of the container. + pub id: String, + /// The image the container is based on. + pub image: String, + /// The docker labels on the container. + pub labels: HashMap, + /// The state of the container, like `running` or `not_deployed` + pub state: DockerContainerState, + /// The status string of the docker container. + pub status: Option, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DockerContainerStats { + #[serde(alias = "Name")] + pub name: String, + #[serde(alias = "CPUPerc")] + pub cpu_perc: String, + #[serde(alias = "MemPerc")] + pub mem_perc: String, + #[serde(alias = "MemUsage")] + pub mem_usage: String, + #[serde(alias = "NetIO")] + pub net_io: String, + #[serde(alias = "BlockIO")] + pub block_io: String, + #[serde(alias = "PIDs")] + pub pids: String, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, + Display, + EnumString, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DockerContainerState { + #[default] + Unknown, + NotDeployed, + Created, + Restarting, + Running, + Removing, + Paused, + Exited, + Dead, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, + Display, + EnumString, +)] +pub enum RestartMode { + #[default] + #[serde(rename = "no")] + #[strum(serialize = "no")] + NoRestart, + #[serde(rename = "on-failure")] + #[strum(serialize = "on-failure")] + OnFailure, + #[serde(rename = "always")] + #[strum(serialize = "always")] + Always, + #[serde(rename = "unless-stopped")] + #[strum(serialize = "unless-stopped")] + UnlessStopped, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, + Display, + EnumString, +)] +#[serde(rename_all = "UPPERCASE")] +#[strum(serialize_all = "UPPERCASE")] +pub enum TerminationSignal { + #[serde(alias = "1")] + SigHup, + #[serde(alias = "2")] + SigInt, + #[serde(alias = "3")] + SigQuit, + #[default] + #[serde(alias = "15")] + SigTerm, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + Default, + PartialEq, + Eq, + Builder, +)] +pub struct TerminationSignalLabel { + #[builder(default)] + pub signal: TerminationSignal, + #[builder(default)] + pub label: String, +} + +#[typeshare] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct DeploymentActionState { + pub deploying: bool, + pub stopping: bool, + pub starting: bool, + pub removing: bool, + pub renaming: bool, + pub deleting: bool, +} + +#[typeshare] +pub type DeploymentQuery = ResourceQuery; + +#[typeshare] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, +)] +pub struct DeploymentQuerySpecifics { + #[serde(default)] + pub server_ids: Vec, + + #[serde(default)] + pub build_ids: Vec, +} + +impl AddFilters for DeploymentQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + if !self.server_ids.is_empty() { + filters + .insert("config.server_id", doc! { "$in": &self.server_ids }); + } + if !self.build_ids.is_empty() { + filters.insert("config.image.type", "Build"); + filters.insert( + "config.image.params.build_id", + doc! { "$in": &self.build_ids }, + ); + } + } +} diff --git a/client/core/rs/src/entities/logger.rs b/client/core/rs/src/entities/logger.rs new file mode 100644 index 000000000..a8dc6f451 --- /dev/null +++ b/client/core/rs/src/entities/logger.rs @@ -0,0 +1,87 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct LogConfig { + /// The logging level. default: info + #[serde(default)] + pub level: LogLevel, + + /// Controls logging to stdout / stderr + #[serde(default)] + pub stdio: StdioLogMode, + + /// Enable opentelemetry exporting + pub otlp_endpoint: Option, + + #[serde(default = "default_opentelemetry_service_name")] + pub opentelemetry_service_name: String, +} + +fn default_opentelemetry_service_name() -> String { + String::from("Monitor") +} + +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, +)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Trace, + Debug, + #[default] + Info, + Warn, + Error, +} + +impl From for tracing::Level { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Trace => tracing::Level::TRACE, + LogLevel::Debug => tracing::Level::DEBUG, + LogLevel::Info => tracing::Level::INFO, + LogLevel::Warn => tracing::Level::WARN, + LogLevel::Error => tracing::Level::ERROR, + } + } +} + +impl From for LogLevel { + fn from(value: tracing::Level) -> Self { + match value.as_str() { + "trace" => LogLevel::Trace, + "debug" => LogLevel::Debug, + "info" => LogLevel::Info, + "warn" => LogLevel::Warn, + "error" => LogLevel::Error, + _ => LogLevel::Info, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, +)] +#[serde(rename_all = "lowercase")] +pub enum StdioLogMode { + #[default] + Standard, + Json, + None, +} diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs new file mode 100644 index 000000000..9e049e7d8 --- /dev/null +++ b/client/core/rs/src/entities/mod.rs @@ -0,0 +1,414 @@ +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use serde::{Deserialize, Serialize}; +use serror::Serror; +use strum::{Display, EnumString}; +use typeshare::typeshare; + +pub mod alert; +pub mod alerter; +pub mod api_key; +pub mod build; +pub mod builder; +pub mod config; +pub mod deployment; +pub mod logger; +pub mod permission; +pub mod procedure; +pub mod repo; +pub mod resource; +pub mod server; +pub mod server_template; +pub mod tag; +pub mod toml; +pub mod update; +pub mod user; +pub mod user_group; + +#[typeshare(serialized_as = "number")] +pub type I64 = i64; +#[typeshare(serialized_as = "number")] +pub type U64 = u64; +#[typeshare(serialized_as = "any")] +pub type MongoDocument = mungos::mongodb::bson::Document; +#[typeshare(serialized_as = "MongoIdObj")] +pub type MongoId = String; +#[typeshare(serialized_as = "__Serror")] +pub type _Serror = Serror; + +/// Represents an empty json object: `{}` +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct NoData {} + +pub fn all_logs_success(logs: &[update::Log]) -> bool { + for log in logs { + if !log.success { + return false; + } + } + true +} + +pub fn optional_string(string: &str) -> Option { + if string.is_empty() { + None + } else { + Some(string.to_string()) + } +} + +pub fn get_image_name( + build::Build { + name, + config: + build::BuildConfig { + docker_organization, + docker_account, + .. + }, + .. + }: &build::Build, +) -> String { + let name = to_monitor_name(name); + if !docker_organization.is_empty() { + format!("{docker_organization}/{name}") + } else if !docker_account.is_empty() { + format!("{docker_account}/{name}") + } else { + name + } +} + +pub fn to_monitor_name(name: &str) -> String { + name.to_lowercase().replace(' ', "_") +} + +pub fn monitor_timestamp() -> i64 { + unix_timestamp_ms() as i64 +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MongoIdObj { + #[serde(rename = "$oid")] + pub oid: String, +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct __Serror { + pub error: String, + pub trace: Vec, +} + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, +)] +pub struct SystemCommand { + #[serde(default)] + pub path: String, + #[serde(default)] + pub command: String, +} + +impl SystemCommand { + pub fn command(&self) -> Option { + if self.is_none() { + None + } else { + Some(format!("cd {} && {}", self.path, self.command)) + } + } + + pub fn into_option(self) -> Option { + if self.is_none() { + None + } else { + Some(self) + } + } + + pub fn is_none(&self) -> bool { + self.path.is_empty() || self.command.is_empty() + } +} + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct Version { + pub major: i32, + pub minor: i32, + pub patch: i32, +} + +impl ToString for Version { + fn to_string(&self) -> String { + format!("{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl TryFrom<&str> for Version { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let vals = value + .split('.') + .map(|v| { + anyhow::Ok( + v.parse().context("failed at parsing value into i32")?, + ) + }) + .collect::>>()?; + let version = Version { + major: *vals + .first() + .ok_or(anyhow!("must include at least major version"))?, + minor: *vals.get(1).unwrap_or(&0), + patch: *vals.get(2).unwrap_or(&0), + }; + Ok(version) + } +} + +impl Version { + pub fn increment(&mut self) { + self.patch += 1; + } + + pub fn is_none(&self) -> bool { + self.major == 0 && self.minor == 0 && self.patch == 0 + } +} + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, +)] +pub struct EnvironmentVar { + pub variable: String, + pub value: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CloneArgs { + pub name: String, + pub repo: Option, + pub branch: Option, + pub on_clone: Option, + pub on_pull: Option, + pub github_account: Option, +} + +impl From<&self::build::Build> for CloneArgs { + fn from(build: &self::build::Build) -> CloneArgs { + CloneArgs { + name: build.name.clone(), + repo: optional_string(&build.config.repo), + branch: optional_string(&build.config.branch), + on_clone: build.config.pre_build.clone().into_option(), + on_pull: None, + github_account: optional_string(&build.config.github_account), + } + } +} + +impl From<&self::repo::Repo> for CloneArgs { + fn from(repo: &self::repo::Repo) -> CloneArgs { + CloneArgs { + name: repo.name.clone(), + repo: optional_string(&repo.config.repo), + branch: optional_string(&repo.config.branch), + on_clone: repo.config.on_clone.clone().into_option(), + on_pull: repo.config.on_pull.clone().into_option(), + github_account: optional_string(&repo.config.github_account), + } + } +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Display, + EnumString, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum Timelength { + #[serde(rename = "1-sec")] + #[strum(serialize = "1-sec")] + OneSecond, + #[serde(rename = "5-sec")] + #[strum(serialize = "5-sec")] + FiveSeconds, + #[serde(rename = "10-sec")] + #[strum(serialize = "10-sec")] + TenSeconds, + #[serde(rename = "15-sec")] + #[strum(serialize = "15-sec")] + FifteenSeconds, + #[serde(rename = "30-sec")] + #[strum(serialize = "30-sec")] + ThirtySeconds, + #[default] + #[serde(rename = "1-min")] + #[strum(serialize = "1-min")] + OneMinute, + #[serde(rename = "2-min")] + #[strum(serialize = "2-min")] + TwoMinutes, + #[serde(rename = "5-min")] + #[strum(serialize = "5-min")] + FiveMinutes, + #[serde(rename = "10-min")] + #[strum(serialize = "10-min")] + TenMinutes, + #[serde(rename = "15-min")] + #[strum(serialize = "15-min")] + FifteenMinutes, + #[serde(rename = "30-min")] + #[strum(serialize = "30-min")] + ThirtyMinutes, + #[serde(rename = "1-hr")] + #[strum(serialize = "1-hr")] + OneHour, + #[serde(rename = "2-hr")] + #[strum(serialize = "2-hr")] + TwoHours, + #[serde(rename = "6-hr")] + #[strum(serialize = "6-hr")] + SixHours, + #[serde(rename = "8-hr")] + #[strum(serialize = "8-hr")] + EightHours, + #[serde(rename = "12-hr")] + #[strum(serialize = "12-hr")] + TwelveHours, + #[serde(rename = "1-day")] + #[strum(serialize = "1-day")] + OneDay, + #[serde(rename = "3-day")] + #[strum(serialize = "3-day")] + ThreeDay, + #[serde(rename = "1-wk")] + #[strum(serialize = "1-wk")] + OneWeek, + #[serde(rename = "2-wk")] + #[strum(serialize = "2-wk")] + TwoWeeks, + #[serde(rename = "30-day")] + #[strum(serialize = "30-day")] + ThirtyDays, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Default, + Display, + EnumString, + PartialEq, + Hash, + Eq, + Clone, + Copy, +)] +pub enum Operation { + // do nothing + #[default] + None, + + // server + CreateServer, + UpdateServer, + DeleteServer, + RenameServer, + PruneImagesServer, + PruneContainersServer, + PruneNetworksServer, + CreateNetwork, + DeleteNetwork, + + // build + CreateBuild, + UpdateBuild, + DeleteBuild, + RunBuild, + CancelBuild, + + // builder + CreateBuilder, + UpdateBuilder, + DeleteBuilder, + + // deployment + CreateDeployment, + UpdateDeployment, + DeleteDeployment, + DeployContainer, + StopContainer, + StopAllContainers, + StartContainer, + RemoveContainer, + RenameDeployment, + + // repo + CreateRepo, + UpdateRepo, + DeleteRepo, + CloneRepo, + PullRepo, + + // alerter + CreateAlerter, + UpdateAlerter, + DeleteAlerter, + + // procedure + CreateProcedure, + UpdateProcedure, + DeleteProcedure, + RunProcedure, + + // server template + CreateServerTemplate, + UpdateServerTemplate, + DeleteServerTemplate, + LaunchServer, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Default, + Display, + EnumString, + PartialEq, + Hash, + Eq, + Clone, + Copy, +)] +pub enum SearchCombinator { + #[default] + Or, + And, +} diff --git a/client/core/rs/src/entities/permission.rs b/client/core/rs/src/entities/permission.rs new file mode 100644 index 000000000..d95100ba1 --- /dev/null +++ b/client/core/rs/src/entities/permission.rs @@ -0,0 +1,105 @@ +use derive_variants::EnumVariants; +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + doc, serde_helpers::hex_string_as_object_id, Document, +}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString}; +use typeshare::typeshare; + +use super::{update::ResourceTarget, MongoId}; + +/// Representation of a User or UserGroups permission on a resource. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, MongoIndexed)] +// To query for all permissions on user target +#[doc_index(doc! { "user_target.type": 1, "user_target.id": 1 })] +// To query for all permissions on a resource target +#[doc_index(doc! { "resource_target.type": 1, "resource_target.id": 1 })] +// Only one permission allowed per user / resource target +#[unique_doc_index(doc! { + "user_target.type": 1, + "user_target.id": 1, + "resource_target.type": 1, + "resource_target.id": 1 +})] +pub struct Permission { + /// The id of the permission document + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: MongoId, + /// The target User / UserGroup + pub user_target: UserTarget, + /// The target resource + pub resource_target: ResourceTarget, + /// The permission level + #[serde(default)] + pub level: PermissionLevel, +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)] +#[variant_derive( + Debug, + Clone, + Copy, + Serialize, + Deserialize, + AsRefStr +)] +#[serde(tag = "type", content = "id")] +pub enum UserTarget { + /// User Id + User(String), + /// UserGroup Id + UserGroup(String), +} + +impl UserTarget { + pub fn extract_variant_id(self) -> (UserTargetVariant, String) { + match self { + UserTarget::User(id) => (UserTargetVariant::User, id), + UserTarget::UserGroup(id) => (UserTargetVariant::UserGroup, id), + } + } +} + +/// The levels of permission that a User or UserGroup can have on a resource. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Display, + EnumString, + AsRefStr, + Hash, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, +)] +pub enum PermissionLevel { + /// No permissions. + #[default] + None, + /// Can see the rousource + Read, + /// Can execute actions on the resource + Execute, + /// Can update the resource configuration + Write, +} + +impl Default for &PermissionLevel { + fn default() -> Self { + &PermissionLevel::None + } +} diff --git a/client/core/rs/src/entities/procedure.rs b/client/core/rs/src/entities/procedure.rs new file mode 100644 index 000000000..032659ea3 --- /dev/null +++ b/client/core/rs/src/entities/procedure.rs @@ -0,0 +1,103 @@ +use derive_default_builder::DefaultBuilder; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString, IntoStaticStr}; +use typeshare::typeshare; + +use crate::api::execute::Execution; + +use super::resource::{ + AddFilters, Resource, ResourceListItem, ResourceQuery, +}; + +// List item + +#[typeshare] +pub type ProcedureListItem = ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ProcedureListItemInfo { + pub procedure_type: ProcedureType, +} + +#[typeshare] +pub type Procedure = Resource; + +#[typeshare(serialized_as = "Partial")] +pub type _PartialProcedureConfig = PartialProcedureConfig; + +#[typeshare] +#[derive(Debug, Clone, Default, Serialize, Deserialize, Partial)] +#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)] +#[partial(skip_serializing_none, from, diff)] +pub struct ProcedureConfig { + /// Whether executions in the procedure runs sequentially or in parallel. + #[serde(default)] + pub procedure_type: ProcedureType, + /// The executions to be run by the procedure. + #[serde(default)] + pub executions: Vec, +} + +#[typeshare] +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Serialize, + Deserialize, + Display, + EnumString, + AsRefStr, + IntoStaticStr, +)] +pub enum ProcedureType { + /// Run the executions one after the other, in order of increasing index. + #[default] + Sequence, + /// Start all the executions simultaneously. + Parallel, +} + +/// Allows to enable / disabled procedures in the sequence / parallel vec on the fly +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct EnabledExecution { + /// The execution request to run. + pub execution: Execution, + /// Whether the execution is enabled to run in the procedure. + pub enabled: bool, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct ProcedureActionState { + pub running: bool, +} + +// QUERY + +#[typeshare] +pub type ProcedureQuery = ResourceQuery; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, +)] +pub struct ProcedureQuerySpecifics { + pub types: Vec, +} + +impl AddFilters for ProcedureQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + let types = + self.types.iter().map(|t| t.as_ref()).collect::>(); + if !self.types.is_empty() { + filters.insert("config.procedure_type", doc! { "$in": types }); + } + } +} diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs new file mode 100644 index 000000000..b08058cd8 --- /dev/null +++ b/client/core/rs/src/entities/repo.rs @@ -0,0 +1,115 @@ +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::I64; + +use super::{ + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + SystemCommand, +}; + +#[typeshare] +pub type RepoListItem = ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct RepoListItemInfo { + pub last_pulled_at: I64, + pub repo: String, + pub branch: String, +} + +#[typeshare] +pub type Repo = Resource; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct RepoInfo { + pub last_pulled_at: I64, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialRepoConfig = PartialRepoConfig; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct RepoConfig { + /// The server to clone the repo on. + #[serde(default, alias = "server")] + #[partial_attr(serde(alias = "server"))] + #[builder(default)] + pub server_id: String, + + /// The github repo to clone. + #[serde(default)] + #[builder(default)] + pub repo: String, + + /// The repo branch. + #[serde(default = "default_branch")] + #[builder(default = "default_branch()")] + #[partial_default(default_branch())] + pub branch: String, + + /// The github account to use to clone. + /// It must be available in the server's periphery config. + #[serde(default)] + #[builder(default)] + pub github_account: String, + + /// Command to be run after the repo is cloned. + /// The path is relative to the root of the repo. + #[serde(default)] + #[builder(default)] + pub on_clone: SystemCommand, + + /// Command to be run after the repo is pulled. + /// The path is relative to the root of the repo. + #[serde(default)] + #[builder(default)] + pub on_pull: SystemCommand, +} + +impl RepoConfig { + pub fn builder() -> RepoConfigBuilder { + RepoConfigBuilder::default() + } +} + +fn default_branch() -> String { + String::from("main") +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct RepoActionState { + pub cloning: bool, + pub pulling: bool, + pub updating: bool, + pub deleting: bool, +} + +#[typeshare] +pub type RepoQuery = ResourceQuery; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, +)] +pub struct RepoQuerySpecifics { + pub repos: Vec, +} + +impl AddFilters for RepoQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + if !self.repos.is_empty() { + filters.insert("config.repo", doc! { "$in": &self.repos }); + } + } +} diff --git a/client/core/rs/src/entities/resource.rs b/client/core/rs/src/entities/resource.rs new file mode 100644 index 000000000..abf28103f --- /dev/null +++ b/client/core/rs/src/entities/resource.rs @@ -0,0 +1,132 @@ +use std::str::FromStr; + +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use mungos::mongodb::bson::{ + doc, oid::ObjectId, serde_helpers::hex_string_as_object_id, + Document, +}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{MongoId, I64}; + +use super::update::ResourceTargetVariant; + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, Builder)] +pub struct Resource { + /// The Mongo ID of the resource. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + #[builder(setter(skip))] + pub id: MongoId, + + /// + pub name: String, + + #[serde(default)] + #[builder(default)] + pub description: String, + + #[serde(default)] + #[builder(setter(skip))] + pub updated_at: I64, + + /// Tag Ids + #[serde(default)] + #[builder(default)] + pub tags: Vec, + + #[serde(default)] + #[builder(setter(skip))] + pub info: Info, + + pub config: Config, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ResourceListItem { + pub id: String, + #[serde(rename = "type")] + pub resource_type: ResourceTargetVariant, + pub name: String, + pub created_at: I64, + /// Tag Ids + pub tags: Vec, + pub info: Info, +} + +/// Passing empty Vec is the same as not filtering by that field +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, +)] +pub struct ResourceQuery { + #[serde(default)] + pub ids: Vec, + #[serde(default)] + pub names: Vec, + /// Pass Vec of tag ids or tag names + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub tag_behavior: TagBehavior, + #[serde(default)] + pub specific: T, +} + +#[typeshare] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub enum TagBehavior { + /// Returns resources which have strictly all the tags + #[default] + All, + /// Returns resources which have one or more of the tags + Any, +} + +pub trait AddFilters { + fn add_filters(&self, _filters: &mut Document) {} +} + +impl AddFilters for () {} + +impl AddFilters for ResourceQuery { + fn add_filters(&self, filters: &mut Document) { + if !self.ids.is_empty() { + let ids = self + .ids + .iter() + .flat_map(|id| ObjectId::from_str(id)) + .collect::>(); + filters.insert("_id", doc! { "$in": &ids }); + } + if !self.names.is_empty() { + filters.insert("name", doc! { "$in": &self.names }); + } + if !self.tags.is_empty() { + match self.tag_behavior { + TagBehavior::All => { + filters.insert("tags", doc! { "$all": &self.tags }); + } + TagBehavior::Any => { + let ors = self + .tags + .iter() + .map(|tag| doc! { "tags": tag }) + .collect::>(); + filters.insert("$or", ors); + } + } + } + self.specific.add_filters(filters); + } +} diff --git a/client/core/rs/src/entities/server/docker_image.rs b/client/core/rs/src/entities/server/docker_image.rs new file mode 100644 index 000000000..86e175dc6 --- /dev/null +++ b/client/core/rs/src/entities/server/docker_image.rs @@ -0,0 +1,97 @@ +use std::collections::HashMap; + +use serde::{ + de::DeserializeOwned, Deserialize, Deserializer, Serialize, +}; +use typeshare::typeshare; + +use crate::entities::I64; + +/// Summary of a docker image cached on a server +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct ImageSummary { + /// ID is the content-addressable ID of an image. This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image). Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. + #[serde(rename = "Id")] + pub id: String, + + /// ID of the parent image. Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. + #[serde(rename = "ParentId")] + pub parent_id: String, + + /// List of image names/tags in the local image cache that reference this image. Multiple image tags can refer to the same image, and this list may be empty if no tags reference the image, in which case the image is \"untagged\", in which case it can still be referenced by its ID. + #[serde(rename = "RepoTags")] + #[serde(deserialize_with = "deserialize_nonoptional_vec")] + pub repo_tags: Vec, + + /// List of content-addressable digests of locally available image manifests that the image is referenced from. Multiple manifests can refer to the same image. These digests are usually only available if the image was either pulled from a registry, or if the image was pushed to a registry, which is when the manifest is generated and its digest calculated. + #[serde(rename = "RepoDigests")] + #[serde(deserialize_with = "deserialize_nonoptional_vec")] + pub repo_digests: Vec, + + /// Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH). + #[serde(rename = "Created")] + pub created: I64, + + /// Total size of the image including all layers it is composed of. + #[serde(rename = "Size")] + pub size: I64, + + /// Total size of image layers that are shared between this image and other images. This size is not calculated by default. `-1` indicates that the value has not been set / calculated. + #[serde(rename = "SharedSize")] + pub shared_size: I64, + + /// Total size of the image including all layers it is composed of. In versions of Docker before v1.10, this field was calculated from the image itself and all of its parent images. Docker v1.10 and up store images self-contained, and no longer use a parent-chain, making this field an equivalent of the Size field. This field is kept for backward compatibility, but may be removed in a future version of the API. + #[serde(rename = "VirtualSize")] + pub virtual_size: Option, + + /// User-defined key/value metadata. + #[serde(rename = "Labels")] + #[serde(deserialize_with = "deserialize_nonoptional_map")] + pub labels: HashMap, + + /// Number of containers using this image. Includes both stopped and running containers. This size is not calculated by default, and depends on which API endpoint is used. `-1` indicates that the value has not been set / calculated. + #[serde(rename = "Containers")] + pub containers: I64, +} + +fn deserialize_nonoptional_vec< + 'de, + D: Deserializer<'de>, + T: DeserializeOwned, +>( + d: D, +) -> Result, D::Error> { + serde::Deserialize::deserialize(d) + .map(|x: Option<_>| x.unwrap_or_default()) +} + +fn deserialize_nonoptional_map< + 'de, + D: Deserializer<'de>, + T: DeserializeOwned, +>( + d: D, +) -> Result, D::Error> { + serde::Deserialize::deserialize(d) + .map(|x: Option<_>| x.unwrap_or_default()) +} + +impl From for ImageSummary { + fn from(value: bollard::service::ImageSummary) -> Self { + Self { + id: value.id, + parent_id: value.parent_id, + repo_tags: value.repo_tags, + repo_digests: value.repo_digests, + created: value.created, + size: value.size, + shared_size: value.shared_size, + virtual_size: value.virtual_size, + labels: value.labels, + containers: value.containers, + } + } +} diff --git a/client/core/rs/src/entities/server/docker_network.rs b/client/core/rs/src/entities/server/docker_network.rs new file mode 100644 index 000000000..8138dd787 --- /dev/null +++ b/client/core/rs/src/entities/server/docker_network.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typeshare::typeshare; + +/// Summary of a docker network on a server. +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct DockerNetwork { + /// The name of the docker network + #[serde(rename = "Name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The Id of the docker network + #[serde(rename = "Id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + + /// Timestamp network created + #[serde(rename = "Created")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "deserialize_timestamp", + serialize_with = "serialize_timestamp" + )] + pub created: Option, + + #[serde(rename = "Scope")] + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + + #[serde(rename = "Driver")] + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + + #[serde(rename = "EnableIPv6")] + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_ipv6: Option, + + #[serde(rename = "IPAM")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ipam: Option, + + #[serde(rename = "Internal")] + #[serde(skip_serializing_if = "Option::is_none")] + pub internal: Option, + + #[serde(rename = "Attachable")] + #[serde(skip_serializing_if = "Option::is_none")] + pub attachable: Option, + + #[serde(rename = "Ingress")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ingress: Option, + + #[serde(rename = "Containers")] + #[serde(skip_serializing_if = "Option::is_none")] + pub containers: Option>, + + #[serde(rename = "Options")] + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, + + #[serde(rename = "Labels")] + #[serde(skip_serializing_if = "Option::is_none")] + pub labels: Option>, +} + +fn deserialize_timestamp<'de, D: Deserializer<'de>>( + d: D, +) -> Result, D::Error> { + serde::Deserialize::deserialize(d) +} + +fn serialize_timestamp( + date: &Option, + s: S, +) -> Result { + match date { + Some(inner) => s.serialize_some(inner), + None => s.serialize_none(), + } +} + +impl From for DockerNetwork { + fn from(value: bollard::service::Network) -> Self { + Self { + name: value.name, + id: value.id, + created: value.created, + scope: value.scope, + driver: value.driver, + enable_ipv6: value.enable_ipv6, + ipam: value.ipam.map(|ipam| ipam.into()), + internal: value.internal, + attachable: value.attachable, + ingress: value.ingress, + containers: value.containers.map(|containers| { + containers.into_iter().map(|(k, v)| (k, v.into())).collect() + }), + options: value.options, + labels: value.labels, + } + } +} + +/// Ipam related information +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct Ipam { + /// Name of the IPAM driver to use. + #[serde(rename = "Driver")] + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + + /// List of IPAM configuration options, specified as a map: ``` {\"Subnet\": , \"IPRange\": , \"Gateway\": , \"AuxAddress\": } ``` + #[serde(rename = "Config")] + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option>, + + /// Driver-specific options, specified as a map. + #[serde(rename = "Options")] + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +impl From for Ipam { + fn from(value: bollard::service::Ipam) -> Self { + Self { + driver: value.driver, + config: value + .config + .map(|config| config.into_iter().map(|c| c.into()).collect()), + options: value.options, + } + } +} + +/// Ipam Configuration. +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct IpamConfig { + #[serde(rename = "Subnet")] + #[serde(skip_serializing_if = "Option::is_none")] + pub subnet: Option, + + #[serde(rename = "IPRange")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ip_range: Option, + + #[serde(rename = "Gateway")] + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway: Option, + + #[serde(rename = "AuxiliaryAddresses")] + #[serde(skip_serializing_if = "Option::is_none")] + pub auxiliary_addresses: Option>, +} + +impl From for IpamConfig { + fn from(value: bollard::service::IpamConfig) -> Self { + Self { + subnet: value.subnet, + ip_range: value.ip_range, + gateway: value.gateway, + auxiliary_addresses: value.auxiliary_addresses, + } + } +} + +/// A container on a network. +#[typeshare] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, +)] +pub struct NetworkContainer { + #[serde(rename = "Name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "EndpointID")] + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint_id: Option, + + #[serde(rename = "MacAddress")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mac_address: Option, + + #[serde(rename = "IPv4Address")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv4_address: Option, + + #[serde(rename = "IPv6Address")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv6_address: Option, +} + +impl From for NetworkContainer { + fn from(value: bollard::service::NetworkContainer) -> Self { + Self { + name: value.name, + endpoint_id: value.endpoint_id, + mac_address: value.mac_address, + ipv4_address: value.ipv4_address, + ipv6_address: value.ipv6_address, + } + } +} diff --git a/client/core/rs/src/entities/server/mod.rs b/client/core/rs/src/entities/server/mod.rs new file mode 100644 index 000000000..c039de400 --- /dev/null +++ b/client/core/rs/src/entities/server/mod.rs @@ -0,0 +1,229 @@ +use derive_builder::Builder; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use super::resource::{ + AddFilters, Resource, ResourceListItem, ResourceQuery, +}; + +pub mod docker_image; +pub mod docker_network; +pub mod stats; + +#[typeshare] +pub type Server = Resource; + +#[typeshare] +pub type ServerListItem = ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ServerListItemInfo { + /// The server's status. + pub status: ServerStatus, + /// Region of the server. + pub region: String, + /// Whether server is configured to send unreachable alerts. + pub send_unreachable_alerts: bool, + /// Whether server is configured to send cpu alerts. + pub send_cpu_alerts: bool, + /// Whether server is configured to send mem alerts. + pub send_mem_alerts: bool, + /// Whether server is configured to send disk alerts. + pub send_disk_alerts: bool, +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialServerConfig = PartialServerConfig; + +/// Server configuration. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +#[partial(skip_serializing_none, from, diff)] +pub struct ServerConfig { + /// The http address of the periphery client. + /// Example: http://localhost:8120 + pub address: String, + + /// Whether a server is enabled. + /// If a server is disabled, + /// you won't be able to perform any actions on it or see deployment's status. + /// default: true + #[serde(default = "default_enabled")] + #[builder(default = "default_enabled()")] + #[partial_default(default_enabled())] + pub enabled: bool, + + /// Whether to monitor any server stats beyond passing health check. + /// default: true + #[serde(default = "default_stats_monitoring")] + #[builder(default = "default_stats_monitoring()")] + #[partial_default(default_stats_monitoring())] + pub stats_monitoring: bool, + + /// Whether to trigger 'docker image prune -a -f' every 24 hours. + /// default: true + #[serde(default = "default_auto_prune")] + #[builder(default = "default_auto_prune()")] + #[partial_default(default_auto_prune())] + pub auto_prune: bool, + + /// Whether to send alerts about the servers reachability + #[serde(default = "default_send_alerts")] + #[builder(default = "default_send_alerts()")] + #[partial_default(default_send_alerts())] + pub send_unreachable_alerts: bool, + + /// Whether to send alerts about the servers CPU status + #[serde(default = "default_send_alerts")] + #[builder(default = "default_send_alerts()")] + #[partial_default(default_send_alerts())] + pub send_cpu_alerts: bool, + + /// Whether to send alerts about the servers MEM status + #[serde(default = "default_send_alerts")] + #[builder(default = "default_send_alerts()")] + #[partial_default(default_send_alerts())] + pub send_mem_alerts: bool, + + /// Whether to send alerts about the servers DISK status + #[serde(default = "default_send_alerts")] + #[builder(default = "default_send_alerts()")] + #[partial_default(default_send_alerts())] + pub send_disk_alerts: bool, + + /// An optional region label + #[serde(default)] + #[builder(default)] + pub region: String, + + /// The percentage threshhold which triggers WARNING state for CPU. + #[serde(default = "default_cpu_warning")] + #[builder(default = "default_cpu_warning()")] + #[partial_default(default_cpu_warning())] + pub cpu_warning: f32, + + /// The percentage threshhold which triggers CRITICAL state for CPU. + #[serde(default = "default_cpu_critical")] + #[builder(default = "default_cpu_critical()")] + #[partial_default(default_cpu_critical())] + pub cpu_critical: f32, + + /// The percentage threshhold which triggers WARNING state for MEM. + #[serde(default = "default_mem_warning")] + #[builder(default = "default_mem_warning()")] + #[partial_default(default_mem_warning())] + pub mem_warning: f64, + + /// The percentage threshhold which triggers CRITICAL state for MEM. + #[serde(default = "default_mem_critical")] + #[builder(default = "default_mem_critical()")] + #[partial_default(default_mem_critical())] + pub mem_critical: f64, + + /// The percentage threshhold which triggers WARNING state for DISK. + #[serde(default = "default_disk_warning")] + #[builder(default = "default_disk_warning()")] + #[partial_default(default_disk_warning())] + pub disk_warning: f64, + + /// The percentage threshhold which triggers CRITICAL state for DISK. + #[serde(default = "default_disk_critical")] + #[builder(default = "default_disk_critical()")] + #[partial_default(default_disk_critical())] + pub disk_critical: f64, +} + +impl ServerConfig { + pub fn builder() -> ServerConfigBuilder { + ServerConfigBuilder::default() + } +} + +fn default_enabled() -> bool { + true +} + +fn default_stats_monitoring() -> bool { + true +} + +fn default_auto_prune() -> bool { + true +} + +fn default_send_alerts() -> bool { + true +} + +fn default_cpu_warning() -> f32 { + 90.0 +} + +fn default_cpu_critical() -> f32 { + 99.0 +} + +fn default_mem_warning() -> f64 { + 75.0 +} + +fn default_mem_critical() -> f64 { + 95.0 +} + +fn default_disk_warning() -> f64 { + 75.0 +} + +fn default_disk_critical() -> f64 { + 95.0 +} + +/// Current pending actions on the server. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct ServerActionState { + /// Server currently pruning networks + pub pruning_networks: bool, + /// Server currently pruning containers + pub pruning_containers: bool, + /// Server currently pruning images + pub pruning_images: bool, + /// Server currently stopping all containers. + pub stopping_containers: bool, +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +pub enum ServerStatus { + /// Server is unreachable. + #[default] + NotOk, + /// Server health check passing. + Ok, + /// Server is disabled. + Disabled, +} + +/// Server-specific query +#[typeshare] +pub type ServerQuery = ResourceQuery; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct ServerQuerySpecifics {} + +impl AddFilters for ServerQuerySpecifics {} diff --git a/client/core/rs/src/entities/server/stats.rs b/client/core/rs/src/entities/server/stats.rs new file mode 100644 index 000000000..1b46df599 --- /dev/null +++ b/client/core/rs/src/entities/server/stats.rs @@ -0,0 +1,172 @@ +use std::{collections::HashMap, path::PathBuf}; + +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::Document; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; +use typeshare::typeshare; + +use crate::entities::{Timelength, I64}; + +/// System information of a server +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct SystemInformation { + /// The system name + pub name: Option, + /// The system long os version + pub os: Option, + /// System's kernel version + pub kernel: Option, + /// Physical core count + pub core_count: Option, + /// System hostname based off DNS + pub host_name: Option, + /// The CPU's brand + pub cpu_brand: String, +} + +/// System stats stored on the database. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +#[collection_name(Stats)] +pub struct SystemStatsRecord { + /// Unix timestamp in milliseconds + #[index] + pub ts: I64, + /// Server id + #[index] + pub sid: String, + // basic stats + /// Cpu usage percentage + pub cpu_perc: f32, + /// Memory used in GB + pub mem_used_gb: f64, + /// Total memory in GB + pub mem_total_gb: f64, + /// Disk used in GB + pub disk_used_gb: f64, + /// Total disk size in GB + pub disk_total_gb: f64, + /// Breakdown of individual disks, ie their usages, sizes, and mount points + pub disks: Vec, +} + +/// Realtime system stats data. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct SystemStats { + /// Cpu usage percentage + pub cpu_perc: f32, + /// Memory used in GB + pub mem_used_gb: f64, + /// Total memory in GB + pub mem_total_gb: f64, + /// Breakdown of individual disks, ie their usages, sizes, and mount points + pub disks: Vec, + + // metadata + /// The rate the system stats are being polled from the system + pub polling_rate: Timelength, + /// Unix timestamp in milliseconds when stats were last polled + pub refresh_ts: I64, + /// Unix timestamp in milliseconds when disk list was last refreshed + pub refresh_list_ts: I64, +} + +/// Info for a single disk mounted on the system. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SingleDiskUsage { + /// The mount point of the disk + pub mount: PathBuf, + /// Used portion of the disk in GB + pub used_gb: f64, + /// Total size of the disk in GB + pub total_gb: f64, +} + +pub fn sum_disk_usage(disks: &[SingleDiskUsage]) -> TotalDiskUsage { + disks + .iter() + .fold(TotalDiskUsage::default(), |mut total, disk| { + total.used_gb += disk.used_gb; + total.total_gb += disk.total_gb; + total + }) +} + +/// Info for the all system disks combined. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct TotalDiskUsage { + /// Used portion in GB + pub used_gb: f64, + /// Total size in GB + pub total_gb: f64, +} + +/// Information about a process on the system. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemProcess { + /// The process PID + pub pid: u32, + /// The process name + pub name: String, + /// The path to the process executable + #[serde(default)] + pub exe: String, + /// The command used to start the process + pub cmd: Vec, + /// The time the process was started + #[serde(default)] + pub start_time: f64, + /// The cpu usage percentage of the process. + /// This is in core-percentage, eg 100% is 1 full core, and + /// an 8 core machine would max at 800%. + pub cpu_perc: f32, + /// The memory usage of the process in MB + pub mem_mb: f64, + /// Process disk read in KB/s + pub disk_read_kb: f64, + /// Process disk write in KB/s + pub disk_write_kb: f64, +} + +/// Summary of the health of the server. +#[typeshare] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct ServerHealth { + pub cpu: SeverityLevel, + pub mem: SeverityLevel, + pub disks: HashMap, +} + +/// Severity level of problem. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Default, + Display, + EnumString, + PartialEq, + Eq, +)] +#[serde(rename_all = "UPPERCASE")] +#[strum(serialize_all = "UPPERCASE")] +pub enum SeverityLevel { + /// No problem. + #[default] + Ok, + /// Problem is imminent. + Warning, + /// Problem fully realized. + Critical, +} diff --git a/client/core/rs/src/entities/server_template.rs b/client/core/rs/src/entities/server_template.rs new file mode 100644 index 000000000..549099593 --- /dev/null +++ b/client/core/rs/src/entities/server_template.rs @@ -0,0 +1,254 @@ +use derive_builder::Builder; +use derive_default_builder::DefaultBuilder; +use derive_variants::EnumVariants; +use mungos::mongodb::bson::{doc, Document}; +use partial_derive2::{MaybeNone, Partial, PartialDiff}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString}; +use typeshare::typeshare; + +use super::{ + builder::AwsBuilderConfig, + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, +}; + +#[typeshare] +pub type ServerTemplate = Resource; + +#[typeshare] +pub type ServerTemplateListItem = + ResourceListItem; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ServerTemplateListItemInfo { + /// The cloud provider + pub provider: String, + /// The instance type, eg c5.2xlarge on for Aws templates + pub instance_type: Option, +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString, + AsRefStr +)] +#[serde(tag = "type", content = "params")] +pub enum ServerTemplateConfig { + /// Template to launch an AWS EC2 instance + Aws(AwsServerTemplateConfig), +} + +impl PartialDiff + for ServerTemplateConfig +{ + fn partial_diff( + &self, + partial: PartialServerTemplateConfig, + ) -> PartialServerTemplateConfig { + match self { + ServerTemplateConfig::Aws(original) => match partial { + PartialServerTemplateConfig::Aws(partial) => { + PartialServerTemplateConfig::Aws( + original.partial_diff(partial), + ) + } + }, + } + } +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + Display, + EnumString, + AsRefStr +)] +#[serde(tag = "type", content = "params")] +pub enum PartialServerTemplateConfig { + Aws(_PartialAwsServerTemplateConfig), +} + +impl MaybeNone for PartialServerTemplateConfig { + fn is_none(&self) -> bool { + match self { + PartialServerTemplateConfig::Aws(config) => config.is_none(), + } + } +} + +impl From for ServerTemplateConfig { + fn from( + value: PartialServerTemplateConfig, + ) -> ServerTemplateConfig { + match value { + PartialServerTemplateConfig::Aws(config) => { + ServerTemplateConfig::Aws(config.into()) + } + } + } +} + +impl From for PartialServerTemplateConfig { + fn from(value: ServerTemplateConfig) -> Self { + match value { + ServerTemplateConfig::Aws(config) => { + PartialServerTemplateConfig::Aws(config.into()) + } + } + } +} + +impl ServerTemplateConfig { + pub fn merge_partial( + self, + partial: PartialServerTemplateConfig, + ) -> ServerTemplateConfig { + match partial { + PartialServerTemplateConfig::Aws(partial) => match self { + ServerTemplateConfig::Aws(config) => { + let config = AwsServerTemplateConfig { + region: partial.region.unwrap_or(config.region), + instance_type: partial + .instance_type + .unwrap_or(config.instance_type), + volumes: partial.volumes.unwrap_or(config.volumes), + ami_id: partial.ami_id.unwrap_or(config.ami_id), + subnet_id: partial.subnet_id.unwrap_or(config.subnet_id), + security_group_ids: partial + .security_group_ids + .unwrap_or(config.security_group_ids), + key_pair_name: partial + .key_pair_name + .unwrap_or(config.key_pair_name), + assign_public_ip: partial + .assign_public_ip + .unwrap_or(config.assign_public_ip), + use_public_ip: partial + .use_public_ip + .unwrap_or(config.use_public_ip), + port: partial.port.unwrap_or(config.port), + }; + ServerTemplateConfig::Aws(config) + } + }, + } + } +} + +#[typeshare(serialized_as = "Partial")] +pub type _PartialAwsServerTemplateConfig = + PartialAwsServerTemplateConfig; + +/// Aws EC2 instance config. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)] +#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)] +#[partial(skip_serializing_none, from, diff)] +pub struct AwsServerTemplateConfig { + /// The aws region to launch the server in, eg. us-east-1 + pub region: String, + /// The instance type to launch, eg. c5.2xlarge + pub instance_type: String, + /// Specify the EBS volumes to attach. + pub volumes: Vec, + /// Specify the ami id to use. Must be set up to start the periphery binary on startup. + pub ami_id: String, + /// The subnet to assign to the instance. + pub subnet_id: String, + /// The security groups to give to the instance. + pub security_group_ids: Vec, + /// The key pair name to give to the instance in case SSH access required. + pub key_pair_name: String, + /// Assign a public ip to the instance. Depending on how your network is + /// setup, this may be required for the instance to reach the public internet. + pub assign_public_ip: bool, + /// Use the instances public ip as the address for the server. + /// Could be used when build instances are created in another non-interconnected network to the core api. + pub use_public_ip: bool, + /// The port periphery will be running on in AMI. + /// Default: `8120` + #[serde(default = "default_port")] + #[builder(default = "default_port()")] + #[partial_default(default_port())] + pub port: i32, +} + +fn default_port() -> i32 { + 8120 +} + +/// For information on AWS volumes, see +/// ``. +#[typeshare] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AwsVolume { + /// The device name (for example, `/dev/sda1` or `xvdh`). + pub device_name: String, + /// The size of the volume in GB + pub size_gb: i32, + /// The type of volume, eg gp2, gp3, io1 + pub volume_type: Option, + /// The iops of the volume, or AWS default. + pub iops: Option, + /// The throughput of the volume, or AWS default. + pub throughput: Option, +} + +impl AwsServerTemplateConfig { + pub fn from_builder_config(value: &AwsBuilderConfig) -> Self { + Self { + region: value.region.clone(), + instance_type: value.instance_type.clone(), + volumes: vec![AwsVolume { + size_gb: value.volume_gb, + device_name: "/dev/sda1".to_string(), + volume_type: None, + iops: None, + throughput: None, + }], + ami_id: value.ami_id.clone(), + subnet_id: value.subnet_id.clone(), + security_group_ids: value.security_group_ids.clone(), + key_pair_name: value.key_pair_name.clone(), + assign_public_ip: value.assign_public_ip, + use_public_ip: value.use_public_ip, + port: value.port, + } + } +} + +#[typeshare] +pub type ServerTemplateQuery = + ResourceQuery; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, +)] +pub struct ServerTemplateQuerySpecifics { + pub types: Vec, +} + +impl AddFilters for ServerTemplateQuerySpecifics { + fn add_filters(&self, filters: &mut Document) { + let types = + self.types.iter().map(|t| t.as_ref()).collect::>(); + if !self.types.is_empty() { + filters.insert("config.type", doc! { "$in": types }); + } + } +} diff --git a/client/core/rs/src/entities/tag.rs b/client/core/rs/src/entities/tag.rs new file mode 100644 index 000000000..cb5166d36 --- /dev/null +++ b/client/core/rs/src/entities/tag.rs @@ -0,0 +1,40 @@ +use derive_builder::Builder; +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + doc, serde_helpers::hex_string_as_object_id, Document, +}; +use partial_derive2::Partial; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::MongoId; + +#[typeshare(serialized_as = "Partial")] +pub type _PartialTag = PartialTag; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Builder, Partial, MongoIndexed, +)] +#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Tag { + /// The Mongo ID of the tag. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized Tag) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + #[builder(setter(skip))] + pub id: MongoId, + + #[unique_index] + pub name: String, + + #[serde(default)] + #[builder(default)] + #[index] + pub owner: String, +} diff --git a/client/core/rs/src/entities/toml.rs b/client/core/rs/src/entities/toml.rs new file mode 100644 index 000000000..8b826f252 --- /dev/null +++ b/client/core/rs/src/entities/toml.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; + +use super::{ + alerter::PartialAlerterConfig, build::PartialBuildConfig, + builder::PartialBuilderConfig, deployment::PartialDeploymentConfig, + permission::PermissionLevel, procedure::PartialProcedureConfig, + repo::PartialRepoConfig, server::PartialServerConfig, + server_template::PartialServerTemplateConfig, + update::ResourceTarget, +}; + +/// Specifies resources to sync on monitor +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ResourcesToml { + #[serde( + default, + rename = "server_template", + skip_serializing_if = "Vec::is_empty" + )] + pub server_templates: + Vec>, + + #[serde( + default, + rename = "server", + skip_serializing_if = "Vec::is_empty" + )] + pub servers: Vec>, + + #[serde( + default, + rename = "build", + skip_serializing_if = "Vec::is_empty" + )] + pub builds: Vec>, + + #[serde( + default, + rename = "deployment", + skip_serializing_if = "Vec::is_empty" + )] + pub deployments: Vec>, + + #[serde( + default, + rename = "builder", + skip_serializing_if = "Vec::is_empty" + )] + pub builders: Vec>, + + #[serde( + default, + rename = "repo", + skip_serializing_if = "Vec::is_empty" + )] + pub repos: Vec>, + + #[serde( + default, + rename = "alerter", + skip_serializing_if = "Vec::is_empty" + )] + pub alerters: Vec>, + + #[serde( + default, + rename = "procedure", + skip_serializing_if = "Vec::is_empty" + )] + pub procedures: Vec>, + + #[serde( + default, + rename = "user_group", + skip_serializing_if = "Vec::is_empty" + )] + pub user_groups: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResourceToml { + /// + pub name: String, + + #[serde(default)] + pub description: String, + + /// Tag ids or names + #[serde(default)] + pub tags: Vec, + + pub config: PartialConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserGroupToml { + pub name: String, + + #[serde(default)] + pub users: Vec, + + #[serde(default, rename = "permission")] + pub permissions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PermissionToml { + pub target: ResourceTarget, + pub level: PermissionLevel, +} diff --git a/client/core/rs/src/entities/update.rs b/client/core/rs/src/entities/update.rs new file mode 100644 index 000000000..92cefbe7d --- /dev/null +++ b/client/core/rs/src/entities/update.rs @@ -0,0 +1,273 @@ +use async_timing_util::unix_timestamp_ms; +use derive_variants::EnumVariants; +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + doc, serde_helpers::hex_string_as_object_id, Document, +}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumString}; +use typeshare::typeshare; + +use crate::entities::{ + all_logs_success, monitor_timestamp, MongoId, Operation, I64, +}; + +use super::{ + alerter::Alerter, build::Build, builder::Builder, + deployment::Deployment, procedure::Procedure, repo::Repo, + server::Server, server_template::ServerTemplate, Version, +}; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +#[doc_index(doc! { "target.type": 1 })] +#[sparse_doc_index(doc! { "target.id": 1 })] +pub struct Update { + /// The Mongo ID of the update. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized Update) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: MongoId, + + #[index] + pub operation: Operation, + + #[index] + pub start_ts: I64, + + #[index] + pub success: bool, + + #[index] + pub operator: String, + + pub target: ResourceTarget, + pub logs: Vec, + pub end_ts: Option, + pub status: UpdateStatus, + pub version: Version, +} + +impl Update { + pub fn push_simple_log( + &mut self, + stage: &str, + msg: impl Into, + ) { + self.logs.push(Log::simple(stage, msg.into())); + } + + pub fn push_error_log( + &mut self, + stage: &str, + msg: impl Into, + ) { + self.logs.push(Log::error(stage, msg.into())); + } + + pub fn in_progress(&mut self) { + self.status = UpdateStatus::InProgress; + } + + pub fn finalize(&mut self) { + self.success = all_logs_success(&self.logs); + self.end_ts = Some(monitor_timestamp()); + self.status = UpdateStatus::Complete; + } +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UpdateListItem { + pub id: String, + pub operation: Operation, + pub start_ts: I64, + pub success: bool, + pub username: String, + pub operator: String, + pub target: ResourceTarget, + pub status: UpdateStatus, + pub version: Version, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Log { + pub stage: String, + pub command: String, + pub stdout: String, + pub stderr: String, + pub success: bool, + pub start_ts: I64, + pub end_ts: I64, +} + +impl Log { + pub fn simple(stage: &str, msg: String) -> Log { + let ts = unix_timestamp_ms() as i64; + Log { + stage: stage.to_string(), + stdout: msg, + success: true, + start_ts: ts, + end_ts: ts, + ..Default::default() + } + } + + pub fn error(stage: &str, msg: String) -> Log { + let ts = unix_timestamp_ms() as i64; + Log { + stage: stage.to_string(), + stderr: msg, + start_ts: ts, + end_ts: ts, + success: false, + ..Default::default() + } + } +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + EnumVariants, +)] +#[variant_derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Display, + EnumString, + AsRefStr +)] +#[serde(tag = "type", content = "id")] +pub enum ResourceTarget { + System(String), + Build(String), + Builder(String), + Deployment(String), + Server(String), + Repo(String), + Alerter(String), + Procedure(String), + ServerTemplate(String), +} + +impl ResourceTarget { + pub fn extract_variant_id( + &self, + ) -> (ResourceTargetVariant, &String) { + let variant: ResourceTargetVariant = self.into(); + let id = match &self { + ResourceTarget::System(id) => id, + ResourceTarget::Build(id) => id, + ResourceTarget::Builder(id) => id, + ResourceTarget::Deployment(id) => id, + ResourceTarget::Server(id) => id, + ResourceTarget::Repo(id) => id, + ResourceTarget::Alerter(id) => id, + ResourceTarget::Procedure(id) => id, + ResourceTarget::ServerTemplate(id) => id, + }; + (variant, id) + } + + pub fn system() -> ResourceTarget { + Self::System("system".to_string()) + } +} + +impl Default for ResourceTarget { + fn default() -> Self { + ResourceTarget::system() + } +} + +impl From<&Build> for ResourceTarget { + fn from(build: &Build) -> Self { + Self::Build(build.id.clone()) + } +} + +impl From<&Deployment> for ResourceTarget { + fn from(deployment: &Deployment) -> Self { + Self::Deployment(deployment.id.clone()) + } +} + +impl From<&Server> for ResourceTarget { + fn from(server: &Server) -> Self { + Self::Server(server.id.clone()) + } +} + +impl From<&Repo> for ResourceTarget { + fn from(repo: &Repo) -> Self { + Self::Repo(repo.id.clone()) + } +} + +impl From<&Builder> for ResourceTarget { + fn from(builder: &Builder) -> Self { + Self::Builder(builder.id.clone()) + } +} + +impl From<&Alerter> for ResourceTarget { + fn from(alerter: &Alerter) -> Self { + Self::Alerter(alerter.id.clone()) + } +} + +impl From<&Procedure> for ResourceTarget { + fn from(procedure: &Procedure) -> Self { + Self::Procedure(procedure.id.clone()) + } +} + +impl From<&ServerTemplate> for ResourceTarget { + fn from(server_template: &ServerTemplate) -> Self { + Self::ServerTemplate(server_template.id.clone()) + } +} + +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Display, + EnumString, + PartialEq, + Hash, + Eq, + Clone, + Copy, + Default, +)] +pub enum UpdateStatus { + Queued, + InProgress, + #[default] + Complete, +} diff --git a/client/core/rs/src/entities/user.rs b/client/core/rs/src/entities/user.rs new file mode 100644 index 000000000..34bd6d030 --- /dev/null +++ b/client/core/rs/src/entities/user.rs @@ -0,0 +1,143 @@ +use std::sync::OnceLock; + +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + doc, serde_helpers::hex_string_as_object_id, Document, +}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{update::ResourceTarget, MongoId, I64}; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +#[doc_index(doc! { "config.type": 1 })] +#[sparse_doc_index(doc! { "config.data.google_id": 1 })] +#[sparse_doc_index(doc! { "config.data.github_id": 1 })] +pub struct User { + /// The Mongo ID of the User. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: MongoId, + + #[unique_index] + pub username: String, + + #[index] + #[serde(default)] + pub enabled: bool, + + #[serde(default)] + pub admin: bool, + + #[serde(default)] + pub create_server_permissions: bool, + + #[serde(default)] + pub create_build_permissions: bool, + + pub config: UserConfig, + + #[serde(default)] + pub last_update_view: I64, + + #[serde(default)] + pub recently_viewed: Vec, + + #[serde(default)] + pub updated_at: I64, +} + +impl User { + /// Prepares user object for transport by removing any sensitive fields + pub fn sanitize(&mut self) { + if let UserConfig::Local { .. } = &self.config { + self.config = UserConfig::default(); + } + } + + pub fn is_service_user(user_id: &str) -> bool { + matches!(user_id, "Procedure" | "Github" | "Auto Redeploy") + } +} + +pub fn admin_service_user(user_id: &str) -> Option { + match user_id { + "Procedure" => procedure_user().to_owned().into(), + "Github" => github_user().to_owned().into(), + "Auto Redeploy" => auto_redeploy_user().to_owned().into(), + _ => None, + } +} + +pub fn procedure_user() -> &'static User { + static PROCEDURE_USER: OnceLock = OnceLock::new(); + PROCEDURE_USER.get_or_init(|| { + let id_name = String::from("Procedure"); + User { + id: id_name.clone(), + username: id_name, + admin: true, + ..Default::default() + } + }) +} + +pub fn github_user() -> &'static User { + static PROCEDURE_USER: OnceLock = OnceLock::new(); + PROCEDURE_USER.get_or_init(|| { + let id_name = String::from("Github"); + User { + id: id_name.clone(), + username: id_name, + admin: true, + ..Default::default() + } + }) +} + +pub fn auto_redeploy_user() -> &'static User { + static AUTO_REDEPLOY_USER: OnceLock = OnceLock::new(); + AUTO_REDEPLOY_USER.get_or_init(|| { + let id_name = String::from("Auto Redeploy"); + User { + id: id_name.clone(), + username: id_name, + admin: true, + ..Default::default() + } + }) +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum UserConfig { + /// User that logs in with username / password + Local { password: String }, + + /// User that logs in via Google Oauth + Google { google_id: String, avatar: String }, + + /// User that logs in via Github Oauth + Github { github_id: String, avatar: String }, + + /// Non-human managed user, can have it's own permissions / api keys + Service { description: String }, +} + +impl Default for UserConfig { + fn default() -> Self { + Self::Local { + password: String::new(), + } + } +} diff --git a/client/core/rs/src/entities/user_group.rs b/client/core/rs/src/entities/user_group.rs new file mode 100644 index 000000000..a14a4e58f --- /dev/null +++ b/client/core/rs/src/entities/user_group.rs @@ -0,0 +1,35 @@ +use mongo_indexed::derive::MongoIndexed; +use mungos::mongodb::bson::{ + serde_helpers::hex_string_as_object_id, Document, +}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use super::{MongoId, I64}; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, MongoIndexed, +)] +pub struct UserGroup { + /// The Mongo ID of the UserGroup. + /// This field is de/serialized from/to JSON as + /// `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + #[serde( + default, + rename = "_id", + skip_serializing_if = "String::is_empty", + with = "hex_string_as_object_id" + )] + pub id: MongoId, + + #[unique_index] + pub name: String, + + /// User ids + #[index] + pub users: Vec, + + #[serde(default)] + pub updated_at: I64, +} diff --git a/client/core/rs/src/lib.rs b/client/core/rs/src/lib.rs new file mode 100644 index 000000000..c8ff0bdd3 --- /dev/null +++ b/client/core/rs/src/lib.rs @@ -0,0 +1,68 @@ +//! # Monitor +//! *A system to build and deploy software accross many servers* +//! +//! This is a client library for the monitor core API. +//! It contains the definitions for all application [entities] +//! (eg. [Build][entities::build::Build], [Deployment][entities::deployment::Deployment], [Server][entities::server::Server], etc.), +//! api requests / responses (eg. [Deploy][api::execute::Deploy], [GetBuild][api::read::GetBuild]), +//! and usage guides. + +use anyhow::Context; +use api::read::GetVersion; +use serde::Deserialize; + +pub mod api; +pub mod busy; +pub mod entities; +pub mod ws; + +mod request; + +#[derive(Deserialize)] +struct MonitorEnv { + monitor_address: String, + monitor_api_key: String, + monitor_api_secret: String, +} + +#[derive(Clone)] +pub struct MonitorClient { + reqwest: reqwest::Client, + address: String, + key: String, + secret: String, +} + +impl MonitorClient { + #[tracing::instrument(skip_all)] + pub async fn new( + address: impl Into, + key: impl Into, + secret: impl Into, + ) -> anyhow::Result { + let client = MonitorClient { + reqwest: Default::default(), + address: address.into(), + key: key.into(), + secret: secret.into(), + }; + client.read(GetVersion {}).await?; + Ok(client) + } + + #[tracing::instrument] + pub async fn new_from_env() -> anyhow::Result { + let MonitorEnv { + monitor_address, + monitor_api_key, + monitor_api_secret, + } = envy::from_env() + .context("failed to parse environment for monitor client")?; + MonitorClient::new( + monitor_address, + monitor_api_key, + monitor_api_secret, + ) + .await + } +} diff --git a/client/core/rs/src/request.rs b/client/core/rs/src/request.rs new file mode 100644 index 000000000..77f526c65 --- /dev/null +++ b/client/core/rs/src/request.rs @@ -0,0 +1,120 @@ +use anyhow::{anyhow, Context}; +use reqwest::StatusCode; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::json; +use serror::deserialize_error; + +use crate::{ + api::{ + auth::MonitorAuthRequest, execute::MonitorExecuteRequest, + read::MonitorReadRequest, write::MonitorWriteRequest, + }, + MonitorClient, +}; + +impl MonitorClient { + #[tracing::instrument(skip(self))] + pub async fn auth( + &self, + request: T, + ) -> anyhow::Result { + self + .post( + "/auth", + json!({ + "type": T::req_type(), + "params": request + }), + ) + .await + } + + #[tracing::instrument(skip(self))] + pub async fn read( + &self, + request: T, + ) -> anyhow::Result { + self + .post( + "/read", + json!({ + "type": T::req_type(), + "params": request + }), + ) + .await + } + + #[tracing::instrument(skip(self))] + pub async fn write( + &self, + request: T, + ) -> anyhow::Result { + self + .post( + "/write", + json!({ + "type": T::req_type(), + "params": request + }), + ) + .await + } + + #[tracing::instrument(skip(self))] + pub async fn execute( + &self, + request: T, + ) -> anyhow::Result { + self + .post( + "/execute", + json!({ + "type": T::req_type(), + "params": request + }), + ) + .await + } + + #[tracing::instrument(skip(self))] + async fn post< + B: Serialize + std::fmt::Debug, + R: DeserializeOwned, + >( + &self, + endpoint: &str, + body: B, + ) -> anyhow::Result { + let req = self + .reqwest + .post(format!("{}{endpoint}", self.address)) + .header("x-api-key", &self.key) + .header("x-api-secret", &self.secret) + .header("Content-Type", "application/json") + .json(&body); + let res = + req.send().await.context("failed to reach monitor api")?; + tracing::debug!("got response"); + let status = res.status(); + if status == StatusCode::OK { + tracing::debug!("response is OK"); + match res.json().await { + Ok(res) => Ok(res), + Err(e) => Err(anyhow!("{status} | {e:#?}")), + } + } else { + tracing::debug!("response is non-OK"); + match res.text().await { + Ok(res) => Err( + deserialize_error(res) + .context(format!("request failed with status {status}")), + ), + Err(e) => Err( + anyhow!("{e:?}") + .context(format!("request failed with status {status}")), + ), + } + } + } +} diff --git a/client/core/rs/src/ws.rs b/client/core/rs/src/ws.rs new file mode 100644 index 000000000..c9a151947 --- /dev/null +++ b/client/core/rs/src/ws.rs @@ -0,0 +1,272 @@ +use std::time::Duration; + +use anyhow::Context; +use futures::{SinkExt, TryStreamExt}; +use serde::{Deserialize, Serialize}; +use serror::serialize_error; +use thiserror::Error; +use tokio::sync::broadcast; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use tokio_util::sync::CancellationToken; +use tracing::{info, info_span, warn, Instrument}; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{entities::update::UpdateListItem, MonitorClient}; + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "params")] +pub enum WsLoginMessage { + Jwt { jwt: String }, + ApiKeys { key: String, secret: String }, +} + +impl WsLoginMessage { + pub fn from_json_str(json: &str) -> anyhow::Result { + serde_json::from_str(json) + .context("failed to parse json as WsLoginMessage") + } + + pub fn to_json_string(&self) -> anyhow::Result { + serde_json::to_string(self) + .context("failed to serialize WsLoginMessage to json string") + } +} + +#[derive(Debug, Clone)] +pub enum UpdateWsMessage { + Update(UpdateListItem), + Error(UpdateWsError), + Disconnected, + Reconnected, +} + +#[derive(Error, Debug, Clone)] +pub enum UpdateWsError { + #[error("failed to connect | {0}")] + ConnectionError(String), + #[error("failed to login | {0}")] + LoginError(String), + #[error("failed to recieve message | {0}")] + MessageError(String), + #[error("did not recognize message | {0}")] + MessageUnrecognized(String), +} + +const MAX_SHORT_RETRY_COUNT: usize = 5; + +impl MonitorClient { + pub fn subscribe_to_updates( + &self, + capacity: usize, + retry_cooldown_secs: u64, + ) -> anyhow::Result<( + broadcast::Receiver, + CancellationToken, + )> { + let (tx, rx) = broadcast::channel(capacity); + let cancel = CancellationToken::new(); + let cancel_clone = cancel.clone(); + let address = + format!("{}/ws/update", self.address.replacen("http", "ws", 1)); + let login_msg = WsLoginMessage::ApiKeys { + key: self.key.clone(), + secret: self.secret.clone(), + } + .to_json_string()?; + + tokio::spawn(async move { + let master_uuid = Uuid::new_v4(); + loop { + // OUTER LOOP (LONG RECONNECT) + if cancel.is_cancelled() { + break; + } + + let outer_uuid = Uuid::new_v4(); + let span = info_span!( + "Outer Loop", + master_uuid = format!("{master_uuid}"), + outer_uuid = format!("{outer_uuid}") + ); + + async { + info!("Entering inner (connection) loop | outer uuid {outer_uuid} | master uuid {master_uuid}"); + let mut retry = 0; + loop { + // INNER LOOP (SHORT RECONNECT) + if cancel.is_cancelled() { + break; + } + if retry >= MAX_SHORT_RETRY_COUNT { + break; + } + + let inner_uuid = Uuid::new_v4(); + let span = info_span!( + "Inner Loop", + master_uuid = format!("{master_uuid}"), + outer_uuid = format!("{outer_uuid}"), + inner_uuid = format!("{inner_uuid}") + ); + + async { + info!("Connecting to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + + let mut ws = + match connect_async(&address).await.with_context(|| { + format!( + "failed to connect to monitor websocket at {address}" + ) + }) { + Ok((ws, _)) => ws, + Err(e) => { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::ConnectionError(serialize_error(&e)), + )); + warn!("{e:#}"); + retry += 1; + return; + } + }; + + info!("Connected to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + + // ================== + // SEND LOGIN MSG + // ================== + let login_send_res = ws + .send(Message::Text(login_msg.clone())) + .await + .context("failed to send login message"); + + if let Err(e) = login_send_res { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::LoginError(serialize_error(&e)), + )); + warn!("breaking inner loop | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + retry += 1; + return; + } + + // ================== + // HANDLE LOGIN RES + // ================== + match ws.try_next().await { + Ok(Some(Message::Text(msg))) => { + if msg != "LOGGED_IN" { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::LoginError(msg.clone()), + )); + let _ = ws.close(None).await; + warn!("breaking inner loop | got msg {msg} instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + retry += 1; + return; + } + } + Ok(Some(msg)) => { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::LoginError(format!("{msg:#?}")), + )); + let _ = ws.close(None).await; + warn!("breaking inner loop | got msg {msg} instead of Message::Text 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + retry += 1; + return; + } + Ok(None) => { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::LoginError(String::from( + "got None message after login message", + )), + )); + let _ = ws.close(None).await; + warn!("breaking inner loop | got None instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + retry += 1; + return; + } + Err(e) => { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::LoginError(format!( + "failed to recieve message | {e:#?}" + )), + )); + let _ = ws.close(None).await; + warn!("breaking inner loop | got error msg | {e:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + retry += 1; + return; + } + } + + let _ = tx.send(UpdateWsMessage::Reconnected); + + info!("logged into websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"); + + // If we get to this point (connected / logged in) reset the short retry counter + retry = 0; + + // ================== + // HANLDE MSGS + // ================== + loop { + match ws + .try_next() + .await + .context("failed to recieve message") + { + Ok(Some(Message::Text(msg))) => { + match serde_json::from_str::(&msg) { + Ok(msg) => { + tracing::debug!( + "got recognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}" + ); + let _ = tx.send(UpdateWsMessage::Update(msg)); + } + Err(_) => { + tracing::warn!( + "got unrecognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}" + ); + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::MessageUnrecognized(msg), + )); + } + } + } + Ok(Some(Message::Close(_))) => { + let _ = tx.send(UpdateWsMessage::Disconnected); + let _ = ws.close(None).await; + tracing::warn!( + "breaking inner loop | got close message | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}" + ); + break; + } + Err(e) => { + let _ = tx.send(UpdateWsMessage::Error( + UpdateWsError::MessageError(serialize_error(&e)), + )); + let _ = tx.send(UpdateWsMessage::Disconnected); + let _ = ws.close(None).await; + tracing::warn!( + "breaking inner loop | got error message | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}" + ); + break; + } + Ok(_) => { + // ignore + } + } + } + } + .instrument(span) + .await; + } + }.instrument(span).await; + + tokio::time::sleep(Duration::from_secs(retry_cooldown_secs)) + .await; + } + }); + + Ok((rx, cancel_clone)) + } +} diff --git a/client/core/ts/generate_types.mjs b/client/core/ts/generate_types.mjs new file mode 100644 index 000000000..f877fe65c --- /dev/null +++ b/client/core/ts/generate_types.mjs @@ -0,0 +1,34 @@ +import { exec } from "child_process"; +import { readFileSync, writeFileSync } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +console.log("generating typescript types..."); + +const gen_command = + "RUST_BACKTRACE=1 typeshare . --lang=typescript --output-file=./client/core/ts/src/types.ts"; + +exec(gen_command, (error, _stdout, _stderr) => { + if (error) { + console.error(error); + return; + } + console.log("generated types using typeshare"); + fix_types(); + console.log("finished."); +}); + +function fix_types() { + const types_path = __dirname + "/src/types.ts"; + const contents = readFileSync(types_path); + const fixed = contents + .toString() + .replaceAll("ResourceTargetVariant", 'ResourceTarget["type"]') + .replaceAll("AlertDataVariant", 'AlertData["type"]') + .replaceAll("ProcedureConfigVariant", 'ProcedureConfig["type"]') + .replaceAll("AlerterConfigVariant", 'AlerterConfig["type"]') + .replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]'); + writeFileSync(types_path, fixed); +} diff --git a/client/core/ts/package.json b/client/core/ts/package.json new file mode 100644 index 000000000..89c67fe33 --- /dev/null +++ b/client/core/ts/package.json @@ -0,0 +1,15 @@ +{ + "name": "@monitor/client", + "version": "1.0.0", + "main": "dist/lib.js", + "license": "MIT", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "axios": "^1.4.0" + }, + "devDependencies": { + "typescript": "^5.1.6" + } +} diff --git a/client/core/ts/src/lib.ts b/client/core/ts/src/lib.ts new file mode 100644 index 000000000..0875fb36d --- /dev/null +++ b/client/core/ts/src/lib.ts @@ -0,0 +1,52 @@ +import axios from "axios"; +import { + AuthResponses, + ExecuteResponses, + ReadResponses, + WriteResponses, +} from "./responses"; +import { + AuthRequest, + ExecuteRequest, + ReadRequest, + WriteRequest, +} from "./types"; + +export * as Types from "./types"; + +type InitOptions = + | { type: "jwt"; params: { jwt: string } } + | { type: "api-key"; params: { api_key: string; secret: string } }; + +export function MonitorClient(url: string, options: InitOptions) { + const state = { + jwt: options.type === "jwt" ? options.params.jwt : undefined, + api_key: options.type === "api-key" ? options.params.api_key : undefined, + secret: options.type === "api-key" ? options.params.secret : undefined, + }; + + const request = async (path: string, request: Req) => + await axios + .post(url + path, request, { + headers: { + Authorization: state.jwt, + "X-API-KEY": state.api_key, + "X-API-SECRET": state.secret, + }, + }) + .then(({ data }) => data); + + const auth = async (req: Req) => + await request("/auth", req); + + const read = async (req: Req) => + await request("/read", req); + + const write = async (req: Req) => + await request("/write", req); + + const execute = async (req: Req) => + await request("/execute", req); + + return { request, auth, read, write, execute }; +} diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts new file mode 100644 index 000000000..390e30d5c --- /dev/null +++ b/client/core/ts/src/responses.ts @@ -0,0 +1,228 @@ +import * as Types from "./types"; + +export type AuthResponses = { + GetLoginOptions: Types.GetLoginOptionsResponse; + CreateLocalUser: Types.CreateLocalUserResponse; + LoginLocalUser: Types.LoginLocalUserResponse; + ExchangeForJwt: Types.ExchangeForJwtResponse; + GetUser: Types.GetUserResponse; +}; + +export type ReadResponses = { + GetVersion: Types.GetVersionResponse; + GetCoreInfo: Types.GetCoreInfoResponse; + + // ==== USER ==== + ListUsers: Types.ListUsersResponse; + GetUsername: Types.GetUsernameResponse; + ListApiKeys: Types.ListApiKeysResponse; + ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse; + ListPermissions: Types.ListPermissionsResponse; + GetPermissionLevel: Types.GetPermissionLevelResponse; + ListUserTargetPermissions: Types.ListUserTargetPermissionsResponse; + + // ==== USER GROUP ==== + GetUserGroup: Types.GetUserGroupResponse; + ListUserGroups: Types.ListUserGroupsResponse; + + // ==== SEARCH ==== + FindResources: Types.FindResourcesResponse; + + // ==== PROCEDURE ==== + GetProceduresSummary: Types.GetProceduresSummaryResponse; + GetProcedure: Types.GetProcedureResponse; + GetProcedureActionState: Types.GetProcedureActionStateResponse; + ListProcedures: Types.ListProceduresResponse; + + // ==== SERVER TEMPLATE ==== + GetServerTemplate: Types.GetServerTemplateResponse; + ListServerTemplates: Types.ListServerTemplatesResponse; + GetServerTemplateSummary: Types.GetServerTemplatesSummaryResponse; + + // ==== SERVER ==== + GetServersSummary: Types.GetServersSummaryResponse; + GetServer: Types.GetServerResponse; + ListServers: Types.ListServersResponse; + GetServerStatus: Types.GetServerStatusResponse; + GetPeripheryVersion: Types.GetPeripheryVersionResponse; + GetSystemInformation: Types.GetSystemInformationResponse; + GetDockerContainers: Types.GetDockerContainersResponse; + GetDockerImages: Types.GetDockerImagesResponse; + GetDockerNetworks: Types.GetDockerNetworksResponse; + GetServerActionState: Types.GetServerActionStateResponse; + GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse; + GetAvailableAccounts: Types.GetAvailableAccountsResponse; + GetAvailableSecrets: Types.GetAvailableSecretsResponse; + + // ==== DEPLOYMENT ==== + GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse; + GetDeployment: Types.GetDeploymentResponse; + ListDeployments: Types.ListDeploymentsResponse; + GetDeploymentContainer: Types.GetDeploymentContainerResponse; + GetDeploymentActionState: Types.GetDeploymentActionStateResponse; + GetDeploymentStats: Types.GetDeploymentStatsResponse; + GetLog: Types.GetLogResponse; + SearchLog: Types.SearchLogResponse; + ListCommonExtraArgs: Types.ListCommonExtraArgsResponse; + + // ==== BUILD ==== + GetBuildsSummary: Types.GetBuildsSummaryResponse; + GetBuild: Types.GetBuildResponse; + ListBuilds: Types.ListBuildsResponse; + GetBuildActionState: Types.GetBuildActionStateResponse; + GetBuildMonthlyStats: Types.GetBuildMonthlyStatsResponse; + GetBuildVersions: Types.GetBuildVersionsResponse; + ListDockerOrganizations: Types.ListDockerOrganizationsResponse; + + // ==== REPO ==== + GetReposSummary: Types.GetReposSummaryResponse; + GetRepo: Types.GetRepoResponse; + ListRepos: Types.ListReposResponse; + GetRepoActionState: Types.GetRepoActionStateResponse; + + // ==== BUILDER ==== + GetBuildersSummary: Types.GetBuildersSummaryResponse; + GetBuilder: Types.GetBuilderResponse; + ListBuilders: Types.ListBuildersResponse; + GetBuilderAvailableAccounts: Types.GetBuilderAvailableAccountsResponse; + + // ==== ALERTER ==== + GetAlertersSummary: Types.GetAlertersSummaryResponse; + GetAlerter: Types.GetAlerterResponse; + ListAlerters: Types.ListAlertersResponse; + + // ==== TOML ==== + ExportAllResourcesToToml: Types.ExportAllResourcesToTomlResponse; + ExportResourcesToToml: Types.ExportResourcesToTomlResponse; + + // ==== TAG ==== + GetTag: Types.GetTagResponse; + ListTags: Types.ListTagsResponse; + + // ==== UPDATE ==== + GetUpdate: Types.GetUpdateResponse; + ListUpdates: Types.ListUpdatesResponse; + + // ==== ALERT ==== + ListAlerts: Types.ListAlertsResponse; + GetAlert: Types.GetAlertResponse; + + // ==== SERVER STATS ==== + GetSystemStats: Types.GetSystemStatsResponse; + GetSystemProcesses: Types.GetSystemProcessesResponse; +}; + +export type WriteResponses = { + // ==== API KEY ==== + CreateApiKey: Types.CreateApiKeyResponse; + DeleteApiKey: Types.DeleteApiKeyResponse; + CreateApiKeyForServiceUser: Types.CreateApiKeyForServiceUserResponse; + DeleteApiKeyForServiceUser: Types.DeleteApiKeyForServiceUserResponse; + + // ==== USER ==== + PushRecentlyViewed: Types.PushRecentlyViewedResponse; + SetLastSeenUpdate: Types.SetLastSeenUpdateResponse; + CreateServiceUser: Types.CreateServiceUserResponse; + UpdateServiceUserDescription: Types.UpdateServiceUserDescription; + + // ==== USER GROUP ==== + CreateUserGroup: Types.UserGroup; + RenameUserGroup: Types.UserGroup; + DeleteUserGroup: Types.UserGroup; + AddUserToUserGroup: Types.UserGroup; + RemoveUserFromUserGroup: Types.UserGroup; + SetUsersInUserGroup: Types.UserGroup; + + // ==== PERMISSIONS ==== + UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse; + UpdatePermissionOnTarget: Types.UpdatePermissionOnTargetResponse; + + // ==== DESCRIPTION ==== + UpdateDescription: Types.UpdateDescriptionResponse; + + // ==== SERVER ==== + LaunchServer: Types.Update; + CreateServer: Types.Server; + DeleteServer: Types.Server; + UpdateServer: Types.Server; + RenameServer: Types.Update; + CreateNetwork: Types.Update; + DeleteNetwork: Types.Update; + + // ==== DEPLOYMENT ==== + CreateDeployment: Types.Deployment; + CopyDeployment: Types.Deployment; + DeleteDeployment: Types.Deployment; + UpdateDeployment: Types.Deployment; + RenameDeployment: Types.Update; + + // ==== BUILD ==== + CreateBuild: Types.Build; + CopyBuild: Types.Build; + DeleteBuild: Types.Build; + UpdateBuild: Types.Build; + + // ==== BUILDER ==== + CreateBuilder: Types.Builder; + CopyBuilder: Types.Builder; + DeleteBuilder: Types.Builder; + UpdateBuilder: Types.Builder; + + // ==== SERVER TEMPLATE ==== + CreateServerTemplate: Types.ServerTemplate; + CopyServerTemplate: Types.ServerTemplate; + DeleteServerTemplate: Types.ServerTemplate; + UpdateServerTemplate: Types.ServerTemplate; + + // ==== REPO ==== + CreateRepo: Types.Repo; + CopyRepo: Types.Repo; + DeleteRepo: Types.Repo; + UpdateRepo: Types.Repo; + + // ==== ALERTER ==== + CreateAlerter: Types.Alerter; + CopyAlerter: Types.Alerter; + DeleteAlerter: Types.Alerter; + UpdateAlerter: Types.Alerter; + + // ==== PROCEDURE ==== + CreateProcedure: Types.Procedure; + CopyProcedure: Types.Procedure; + DeleteProcedure: Types.Procedure; + UpdateProcedure: Types.Procedure; + + // ==== TAG ==== + CreateTag: Types.Tag; + DeleteTag: Types.Tag; + RenameTag: Types.Tag; + UpdateTagsOnResource: Types.UpdateTagsOnResourceResponse; +}; + +export type ExecuteResponses = { + // ==== SERVER ==== + PruneContainers: Types.Update; + PruneImages: Types.Update; + PruneNetworks: Types.Update; + + // ==== DEPLOYMENT ==== + Deploy: Types.Update; + StartContainer: Types.Update; + StopContainer: Types.Update; + StopAllContainers: Types.Update; + RemoveContainer: Types.Update; + + // ==== BUILD ==== + RunBuild: Types.Update; + CancelBuild: Types.CancelBuildResponse; + + // ==== REPO ==== + CloneRepo: Types.Update; + PullRepo: Types.Update; + + // ==== PROCEDURE ==== + RunProcedure: Types.Update; + + // ==== SERVER TEMPLATE ==== + LaunchServer: Types.Update; +}; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts new file mode 100644 index 000000000..c294ee718 --- /dev/null +++ b/client/core/ts/src/types.ts @@ -0,0 +1,3244 @@ +/* + Generated by typeshare 1.7.0 +*/ + +/** JSON containing an authentication token. */ +export interface JwtResponse { + /** A token the user can use to authenticate their requests. */ + jwt: string; +} + +/** Response for [CreateLocalUser]. */ +export type CreateLocalUserResponse = JwtResponse; + +/** The response for [LoginLocalUser] */ +export type LoginLocalUserResponse = JwtResponse; + +/** Response for [ExchangeForJwt]. */ +export type ExchangeForJwtResponse = JwtResponse; + +export interface MongoIdObj { + $oid: string; +} + +export type MongoId = MongoIdObj; + +export type UserConfig = + /** User that logs in with username / password */ + | { type: "Local", data: { + password: string; +}} + /** User that logs in via Google Oauth */ + | { type: "Google", data: { + google_id: string; + avatar: string; +}} + /** User that logs in via Github Oauth */ + | { type: "Github", data: { + github_id: string; + avatar: string; +}} + /** Non-human managed user, can have it's own permissions / api keys */ + | { type: "Service", data: { + description: string; +}}; + +export type I64 = number; + +export type ResourceTarget = + | { type: "System", id: string } + | { type: "Build", id: string } + | { type: "Builder", id: string } + | { type: "Deployment", id: string } + | { type: "Server", id: string } + | { type: "Repo", id: string } + | { type: "Alerter", id: string } + | { type: "Procedure", id: string } + | { type: "ServerTemplate", id: string }; + +export interface User { + /** + * The Mongo ID of the User. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + */ + _id?: MongoId; + username: string; + enabled?: boolean; + admin?: boolean; + create_server_permissions?: boolean; + create_build_permissions?: boolean; + config: UserConfig; + last_update_view?: I64; + recently_viewed?: ResourceTarget[]; + updated_at?: I64; +} + +export type GetUserResponse = User; + +/** Represents an empty json object: `{}` */ +export interface NoData { +} + +export type CancelBuildResponse = NoData; + +/** Severity level of problem. */ +export enum SeverityLevel { + /** No problem. */ + Ok = "OK", + /** Problem is imminent. */ + Warning = "WARNING", + /** Problem fully realized. */ + Critical = "CRITICAL", +} + +/** The variants of data related to the alert. */ +export type AlertData = + /** A server could not be reached. */ + | { type: "ServerUnreachable", data: { + /** The id of the server */ + id: string; + /** The name of the server */ + name: string; + /** The region of the server */ + region?: string; + /** The error data */ + err?: _Serror; +}} + /** A server has high CPU usage. */ + | { type: "ServerCpu", data: { + /** The id of the server */ + id: string; + /** The name of the server */ + name: string; + /** The region of the server */ + region?: string; + /** The cpu usage percentage */ + percentage: number; +}} + /** A server has high memory usage. */ + | { type: "ServerMem", data: { + /** The id of the server */ + id: string; + /** The name of the server */ + name: string; + /** The region of the server */ + region?: string; + /** The used memory */ + used_gb: number; + /** The total memory */ + total_gb: number; +}} + /** A server has high disk usage. */ + | { type: "ServerDisk", data: { + /** The id of the server */ + id: string; + /** The name of the server */ + name: string; + /** The region of the server */ + region?: string; + /** The mount path of the disk */ + path: string; + /** The used portion of the disk in GB */ + used_gb: number; + /** The total size of the disk in GB */ + total_gb: number; +}} + /** A container's state has changed unexpectedly. */ + | { type: "ContainerStateChange", data: { + /** The id of the deployment */ + id: string; + /** The name of the deployment */ + name: string; + /** The server id of server deployment is on */ + server_id: string; + /** The server name */ + server_name: string; + /** The previous container state */ + from: DockerContainerState; + /** The current container state */ + to: DockerContainerState; +}} + /** An AWS builder failed to terminate. */ + | { type: "AwsBuilderTerminationFailed", data: { + /** The id of the aws instance which failed to terminate */ + instance_id: string; +}} + | { type: "None", data: { +}}; + +/** Representation of an alert in the system. */ +export interface Alert { + /** + * The Mongo ID of the alert. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized Alert) }` + */ + _id?: MongoId; + /** Unix timestamp in milliseconds the alert was opened */ + ts: I64; + /** Whether the alert is already resolved */ + resolved: boolean; + /** The severity of the alert */ + level: SeverityLevel; + /** The target of the alert */ + target: ResourceTarget; + /** The type of alert, eg ServerUnreachable, ServerMem, ContainerStateChange */ + variant: AlertData["type"]; + /** The data attached to the alert */ + data: AlertData; + /** The timestamp of alert resolution */ + resolved_ts?: I64; +} + +export type GetAlertResponse = Alert; + +export interface Resource { + /** + * The Mongo ID of the resource. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource) }` + */ + _id?: MongoId; + /** */ + name: string; + description?: string; + updated_at?: I64; + /** Tag Ids */ + tags?: string[]; + info?: Info; + config: Config; +} + +export type AlerterConfig = + /** Send alert serialized to JSON to an http endpoint. */ + | { type: "Custom", params: CustomAlerterConfig } + /** Send alert to a slack app */ + | { type: "Slack", params: SlackAlerterConfig }; + +export interface AlerterInfo { + is_default?: boolean; +} + +export type Alerter = Resource; + +export type GetAlerterResponse = Alerter; + +export interface ResourceListItem { + id: string; + type: ResourceTarget["type"]; + name: string; + created_at: I64; + /** Tag Ids */ + tags: string[]; + info: Info; +} + +export interface AlerterListItemInfo { + /** Whether alerter is enabled for sending alerts */ + enabled: boolean; + /** Whether the alerter is the default */ + is_default: boolean; + /** The type of the alerter, eg. Slack, Custom */ + alerter_type: string; +} + +export type AlerterListItem = ResourceListItem; + +export type ListAlertersResponse = AlerterListItem[]; + +export interface Version { + major: number; + minor: number; + patch: number; +} + +export interface SystemCommand { + path?: string; + command?: string; +} + +export interface EnvironmentVar { + variable: string; + value: string; +} + +/** The build configuration. */ +export interface BuildConfig { + /** Which builder is used to build the image. */ + builder_id?: string; + /** Whether to skip secret interpolation in the build_args. */ + skip_secret_interp?: boolean; + /** The current version of the build. */ + version?: Version; + /** The Github repo used as the source of the build. */ + repo?: string; + /** The branch of the repo. */ + branch: string; + /** + * The github account used to clone (used to access private repos). + * Empty string is public clone (only public repos). + */ + github_account?: string; + /** + * The dockerhub account used to push the image to dockerhub. + * Empty string means no dockerhub push (server local build). + */ + docker_account?: string; + /** + * The docker organization which the image should be pushed under. + * Empty string means no organization. + */ + docker_organization?: string; + /** The optional command run after repo clone and before docker build. */ + pre_build?: SystemCommand; + /** + * The path of the docker build context relative to the root of the repo. + * Default: "." (the root of the repo). + */ + build_path: string; + /** The path of the dockerfile relative to the build path. */ + dockerfile_path: string; + /** Docker build arguments */ + build_args?: EnvironmentVar[]; + /** Docker labels */ + labels?: EnvironmentVar[]; + /** Any extra docker cli arguments to be included in the build command */ + extra_args?: string[]; + /** Whether to use buildx to build (eg `docker buildx build ...`) */ + use_buildx?: boolean; +} + +export interface BuildInfo { + last_built_at: I64; +} + +export type Build = Resource; + +export type GetBuildResponse = Build; + +export interface BuildListItemInfo { + /** Unix timestamp in milliseconds of last build */ + last_built_at: I64; + /** The current version of the build */ + version: Version; + /** The Github repo used as the source of the build */ + repo: string; + /** The branch of the repo */ + branch: string; +} + +export type BuildListItem = ResourceListItem; + +export type ListBuildsResponse = BuildListItem[]; + +export interface BuildActionState { + building: boolean; +} + +export type GetBuildActionStateResponse = BuildActionState; + +export interface BuildVersionResponseItem { + version: Version; + ts: I64; +} + +export type GetBuildVersionsResponse = BuildVersionResponseItem[]; + +export type ListDockerOrganizationsResponse = string[]; + +export type BuilderConfig = + /** Use a connected server an image builder. */ + | { type: "Server", params: ServerBuilderConfig } + /** Use EC2 instances spawned on demand as an image builder. */ + | { type: "Aws", params: AwsBuilderConfig }; + +export type Builder = Resource; + +export type GetBuilderResponse = Builder; + +export interface BuilderListItemInfo { + builder_type: string; + instance_type?: string; +} + +export type BuilderListItem = ResourceListItem; + +export type ListBuildersResponse = BuilderListItem[]; + +export type DeploymentImage = + /** Deploy any external image. */ + | { type: "Image", params: { + /** The docker image, can be from any registry that works with docker and that the host server can reach. */ + image?: string; +}} + /** Deploy a monitor build. */ + | { type: "Build", params: { + /** The id of the build */ + build_id?: string; + /** + * Use a custom / older version of the image produced by the build. + * if version is 0.0.0, this means `latest` image. + */ + version?: Version; +}}; + +export enum TerminationSignal { + SigHup = "SIGHUP", + SigInt = "SIGINT", + SigQuit = "SIGQUIT", + SigTerm = "SIGTERM", +} + +export interface TerminationSignalLabel { + signal: TerminationSignal; + label: string; +} + +export interface Conversion { + /** reference on the server. */ + local: string; + /** reference in the container. */ + container: string; +} + +export enum RestartMode { + NoRestart = "no", + OnFailure = "on-failure", + Always = "always", + UnlessStopped = "unless-stopped", +} + +export interface DeploymentConfig { + /** The id of server the deployment is deployed on. */ + server_id?: string; + /** Whether to send ContainerStateChange alerts for this deployment. */ + send_alerts: boolean; + /** + * The image which the deployment deploys. + * Can either be a user inputted image, or a Monitor build. + */ + image?: DeploymentImage; + /** Whether to skip secret interpolation into the deployment environment variables. */ + skip_secret_interp?: boolean; + /** Whether to redeploy the deployment whenever the attached build finishes. */ + redeploy_on_build?: boolean; + /** + * Labels attached to various termination signal options. + * Used to specify different shutdown functionality depending on the termination signal. + */ + term_signal_labels: TerminationSignalLabel[]; + /** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */ + termination_signal?: TerminationSignal; + /** The termination timeout. */ + termination_timeout: number; + /** + * The container port mapping. + * Irrelevant if container network is `host`. + * Maps ports on host to ports on container. + */ + ports?: Conversion[]; + /** + * The container volume mapping. + * Maps files / folders on host to files / folders in container. + */ + volumes?: Conversion[]; + /** The environment variables passed to the container. */ + environment?: EnvironmentVar[]; + /** The docker labels given to the container. */ + labels?: EnvironmentVar[]; + /** + * The network attached to the container. + * Default is `host`. + */ + network: string; + /** The restart mode given to the container. */ + restart?: RestartMode; + /** + * This is interpolated at the end of the `docker run` command, + * which means they are either passed to the containers inner process, + * or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile. + * Empty is no process args. + */ + process_args?: string; + /** + * Extra args which are interpolated into the `docker run` command, + * and affect the container configuration. + */ + extra_args?: string[]; + /** + * The docker account the deployment should use to pull the image. + * - If using a custom image, empty string means don't use an account. Only works for public images. + * - If using a monitor build, empty string means to use the same docker account as the build uses. + */ + docker_account?: string; +} + +export type Deployment = Resource; + +export type GetDeploymentResponse = Deployment; + +export enum DockerContainerState { + Unknown = "unknown", + NotDeployed = "not_deployed", + Created = "created", + Restarting = "restarting", + Running = "running", + Removing = "removing", + Paused = "paused", + Exited = "exited", + Dead = "dead", +} + +export interface DeploymentListItemInfo { + /** The state of the docker container. */ + state: DockerContainerState; + /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ + status?: string; + /** The image attached to the deployment. */ + image: string; + /** The server that deployment sits on. */ + server_id: string; + /** An attached monitor build, if it exists. */ + build_id?: string; +} + +export type DeploymentListItem = ResourceListItem; + +export type ListDeploymentsResponse = DeploymentListItem[]; + +export interface Log { + stage: string; + command: string; + stdout: string; + stderr: string; + success: boolean; + start_ts: I64; + end_ts: I64; +} + +export type GetLogResponse = Log; + +export type SearchLogResponse = Log; + +export interface DockerContainerStats { + name: string; + cpu_perc: string; + mem_perc: string; + mem_usage: string; + net_io: string; + block_io: string; + pids: string; +} + +export type GetDeploymentStatsResponse = DockerContainerStats; + +export interface DeploymentActionState { + deploying: boolean; + stopping: boolean; + starting: boolean; + removing: boolean; + renaming: boolean; + deleting: boolean; +} + +export type GetDeploymentActionStateResponse = DeploymentActionState; + +export type ListCommonExtraArgsResponse = string[]; + +export type UserTarget = + /** User Id */ + | { type: "User", id: string } + /** UserGroup Id */ + | { type: "UserGroup", id: string }; + +/** The levels of permission that a User or UserGroup can have on a resource. */ +export enum PermissionLevel { + /** No permissions. */ + None = "None", + /** Can see the rousource */ + Read = "Read", + /** Can execute actions on the resource */ + Execute = "Execute", + /** Can update the resource configuration */ + Write = "Write", +} + +/** Representation of a User or UserGroups permission on a resource. */ +export interface Permission { + /** The id of the permission document */ + _id?: MongoId; + /** The target User / UserGroup */ + user_target: UserTarget; + /** The target resource */ + resource_target: ResourceTarget; + /** The permission level */ + level?: PermissionLevel; +} + +export type ListPermissionsResponse = Permission[]; + +export type GetPermissionLevelResponse = PermissionLevel; + +export type ListUserTargetPermissionsResponse = Permission[]; + +export enum ProcedureType { + /** Run the executions one after the other, in order of increasing index. */ + Sequence = "Sequence", + /** Start all the executions simultaneously. */ + Parallel = "Parallel", +} + +/** A wrapper for all monitor exections. */ +export type Execution = + /** For new executions upon instantiation */ + | { type: "None", params: NoData } + | { type: "RunProcedure", params: RunProcedure } + | { type: "RunBuild", params: RunBuild } + | { type: "Deploy", params: Deploy } + | { type: "StartContainer", params: StartContainer } + | { type: "StopContainer", params: StopContainer } + | { type: "StopAllContainers", params: StopAllContainers } + | { type: "RemoveContainer", params: RemoveContainer } + | { type: "CloneRepo", params: CloneRepo } + | { type: "PullRepo", params: PullRepo } + | { type: "PruneDockerNetworks", params: PruneDockerNetworks } + | { type: "PruneDockerImages", params: PruneDockerImages } + | { type: "PruneDockerContainers", params: PruneDockerContainers }; + +/** Allows to enable / disabled procedures in the sequence / parallel vec on the fly */ +export interface EnabledExecution { + /** The execution request to run. */ + execution: Execution; + /** Whether the execution is enabled to run in the procedure. */ + enabled: boolean; +} + +export interface ProcedureConfig { + /** Whether executions in the procedure runs sequentially or in parallel. */ + procedure_type?: ProcedureType; + /** The executions to be run by the procedure. */ + executions?: EnabledExecution[]; +} + +export type Procedure = Resource; + +export type GetProcedureResponse = Procedure; + +export interface ProcedureListItemInfo { + procedure_type: ProcedureType; +} + +export type ProcedureListItem = ResourceListItem; + +export type ListProceduresResponse = ProcedureListItem[]; + +export interface ProcedureActionState { + running: boolean; +} + +export type GetProcedureActionStateResponse = ProcedureActionState; + +export interface RepoConfig { + /** The server to clone the repo on. */ + server_id?: string; + /** The github repo to clone. */ + repo?: string; + /** The repo branch. */ + branch: string; + /** + * The github account to use to clone. + * It must be available in the server's periphery config. + */ + github_account?: string; + /** + * Command to be run after the repo is cloned. + * The path is relative to the root of the repo. + */ + on_clone?: SystemCommand; + /** + * Command to be run after the repo is pulled. + * The path is relative to the root of the repo. + */ + on_pull?: SystemCommand; +} + +export interface RepoInfo { + last_pulled_at: I64; +} + +export type Repo = Resource; + +export type GetRepoResponse = Repo; + +export interface RepoListItemInfo { + last_pulled_at: I64; + repo: string; + branch: string; +} + +export type RepoListItem = ResourceListItem; + +export type ListReposResponse = RepoListItem[]; + +export interface RepoActionState { + cloning: boolean; + pulling: boolean; + updating: boolean; + deleting: boolean; +} + +export type GetRepoActionStateResponse = RepoActionState; + +/** Server configuration. */ +export interface ServerConfig { + /** + * The http address of the periphery client. + * Example: http://localhost:8120 + */ + address: string; + /** + * Whether a server is enabled. + * If a server is disabled, + * you won't be able to perform any actions on it or see deployment's status. + * default: true + */ + enabled: boolean; + /** + * Whether to monitor any server stats beyond passing health check. + * default: true + */ + stats_monitoring: boolean; + /** + * Whether to trigger 'docker image prune -a -f' every 24 hours. + * default: true + */ + auto_prune: boolean; + /** Whether to send alerts about the servers reachability */ + send_unreachable_alerts: boolean; + /** Whether to send alerts about the servers CPU status */ + send_cpu_alerts: boolean; + /** Whether to send alerts about the servers MEM status */ + send_mem_alerts: boolean; + /** Whether to send alerts about the servers DISK status */ + send_disk_alerts: boolean; + /** An optional region label */ + region?: string; + /** The percentage threshhold which triggers WARNING state for CPU. */ + cpu_warning: number; + /** The percentage threshhold which triggers CRITICAL state for CPU. */ + cpu_critical: number; + /** The percentage threshhold which triggers WARNING state for MEM. */ + mem_warning: number; + /** The percentage threshhold which triggers CRITICAL state for MEM. */ + mem_critical: number; + /** The percentage threshhold which triggers WARNING state for DISK. */ + disk_warning: number; + /** The percentage threshhold which triggers CRITICAL state for DISK. */ + disk_critical: number; +} + +export type Server = Resource; + +export type GetServerResponse = Server; + +export enum ServerStatus { + /** Server is unreachable. */ + NotOk = "NotOk", + /** Server health check passing. */ + Ok = "Ok", + /** Server is disabled. */ + Disabled = "Disabled", +} + +export interface ServerListItemInfo { + /** The server's status. */ + status: ServerStatus; + /** Region of the server. */ + region: string; + /** Whether server is configured to send unreachable alerts. */ + send_unreachable_alerts: boolean; + /** Whether server is configured to send cpu alerts. */ + send_cpu_alerts: boolean; + /** Whether server is configured to send mem alerts. */ + send_mem_alerts: boolean; + /** Whether server is configured to send disk alerts. */ + send_disk_alerts: boolean; +} + +export type ServerListItem = ResourceListItem; + +export type ListServersResponse = ServerListItem[]; + +/** Current pending actions on the server. */ +export interface ServerActionState { + /** Server currently pruning networks */ + pruning_networks: boolean; + /** Server currently pruning containers */ + pruning_containers: boolean; + /** Server currently pruning images */ + pruning_images: boolean; + /** Server currently stopping all containers. */ + stopping_containers: boolean; +} + +export type GetServerActionStateResponse = ServerActionState; + +/** Ipam Configuration. */ +export interface IpamConfig { + Subnet?: string; + IPRange?: string; + Gateway?: string; + AuxiliaryAddresses?: Record; +} + +/** Ipam related information */ +export interface Ipam { + /** Name of the IPAM driver to use. */ + Driver?: string; + /** List of IPAM configuration options, specified as a map: ``` {\"Subnet\": , \"IPRange\": , \"Gateway\": , \"AuxAddress\": } ``` */ + Config?: IpamConfig[]; + /** Driver-specific options, specified as a map. */ + Options?: Record; +} + +/** A container on a network. */ +export interface NetworkContainer { + Name?: string; + EndpointID?: string; + MacAddress?: string; + IPv4Address?: string; + IPv6Address?: string; +} + +/** Summary of a docker network on a server. */ +export interface DockerNetwork { + /** The name of the docker network */ + Name?: string; + /** The Id of the docker network */ + Id?: string; + /** Timestamp network created */ + Created?: string; + Scope?: string; + Driver?: string; + EnableIPv6?: boolean; + IPAM?: Ipam; + Internal?: boolean; + Attachable?: boolean; + Ingress?: boolean; + Containers?: Record; + Options?: Record; + Labels?: Record; +} + +export type GetDockerNetworksResponse = DockerNetwork[]; + +/** Summary of a docker image cached on a server */ +export interface ImageSummary { + /** ID is the content-addressable ID of an image. This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image). Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. */ + Id: string; + /** ID of the parent image. Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. */ + ParentId: string; + /** List of image names/tags in the local image cache that reference this image. Multiple image tags can refer to the same image, and this list may be empty if no tags reference the image, in which case the image is \"untagged\", in which case it can still be referenced by its ID. */ + RepoTags: string[]; + /** List of content-addressable digests of locally available image manifests that the image is referenced from. Multiple manifests can refer to the same image. These digests are usually only available if the image was either pulled from a registry, or if the image was pushed to a registry, which is when the manifest is generated and its digest calculated. */ + RepoDigests: string[]; + /** Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH). */ + Created: I64; + /** Total size of the image including all layers it is composed of. */ + Size: I64; + /** Total size of image layers that are shared between this image and other images. This size is not calculated by default. `-1` indicates that the value has not been set / calculated. */ + SharedSize: I64; + /** Total size of the image including all layers it is composed of. In versions of Docker before v1.10, this field was calculated from the image itself and all of its parent images. Docker v1.10 and up store images self-contained, and no longer use a parent-chain, making this field an equivalent of the Size field. This field is kept for backward compatibility, but may be removed in a future version of the API. */ + VirtualSize?: I64; + /** User-defined key/value metadata. */ + Labels: Record; + /** Number of containers using this image. Includes both stopped and running containers. This size is not calculated by default, and depends on which API endpoint is used. `-1` indicates that the value has not been set / calculated. */ + Containers: I64; +} + +export type GetDockerImagesResponse = ImageSummary[]; + +/** A summary of a docker container on a server. */ +export interface ContainerSummary { + /** Name of the container. */ + name: string; + /** Id of the container. */ + id: string; + /** The image the container is based on. */ + image: string; + /** The docker labels on the container. */ + labels: Record; + /** The state of the container, like `running` or `not_deployed` */ + state: DockerContainerState; + /** The status string of the docker container. */ + status?: string; +} + +export type GetDockerContainersResponse = ContainerSummary[]; + +/** System information of a server */ +export interface SystemInformation { + /** The system name */ + name?: string; + /** The system long os version */ + os?: string; + /** System's kernel version */ + kernel?: string; + /** Physical core count */ + core_count?: number; + /** System hostname based off DNS */ + host_name?: string; + /** The CPU's brand */ + cpu_brand: string; +} + +export type GetSystemInformationResponse = SystemInformation; + +/** Info for a single disk mounted on the system. */ +export interface SingleDiskUsage { + /** The mount point of the disk */ + mount: string; + /** Used portion of the disk in GB */ + used_gb: number; + /** Total size of the disk in GB */ + total_gb: number; +} + +export enum Timelength { + OneSecond = "1-sec", + FiveSeconds = "5-sec", + TenSeconds = "10-sec", + FifteenSeconds = "15-sec", + ThirtySeconds = "30-sec", + OneMinute = "1-min", + TwoMinutes = "2-min", + FiveMinutes = "5-min", + TenMinutes = "10-min", + FifteenMinutes = "15-min", + ThirtyMinutes = "30-min", + OneHour = "1-hr", + TwoHours = "2-hr", + SixHours = "6-hr", + EightHours = "8-hr", + TwelveHours = "12-hr", + OneDay = "1-day", + ThreeDay = "3-day", + OneWeek = "1-wk", + TwoWeeks = "2-wk", + ThirtyDays = "30-day", +} + +/** Realtime system stats data. */ +export interface SystemStats { + /** Cpu usage percentage */ + cpu_perc: number; + /** Memory used in GB */ + mem_used_gb: number; + /** Total memory in GB */ + mem_total_gb: number; + /** Breakdown of individual disks, ie their usages, sizes, and mount points */ + disks: SingleDiskUsage[]; + /** The rate the system stats are being polled from the system */ + polling_rate: Timelength; + /** Unix timestamp in milliseconds when stats were last polled */ + refresh_ts: I64; + /** Unix timestamp in milliseconds when disk list was last refreshed */ + refresh_list_ts: I64; +} + +export type GetSystemStatsResponse = SystemStats; + +/** Information about a process on the system. */ +export interface SystemProcess { + /** The process PID */ + pid: number; + /** The process name */ + name: string; + /** The path to the process executable */ + exe?: string; + /** The command used to start the process */ + cmd: string[]; + /** The time the process was started */ + start_time?: number; + /** + * The cpu usage percentage of the process. + * This is in core-percentage, eg 100% is 1 full core, and + * an 8 core machine would max at 800%. + */ + cpu_perc: number; + /** The memory usage of the process in MB */ + mem_mb: number; + /** Process disk read in KB/s */ + disk_read_kb: number; + /** Process disk write in KB/s */ + disk_write_kb: number; +} + +export type GetSystemProcessesResponse = SystemProcess[]; + +export type GetAvailableSecretsResponse = string[]; + +export type ServerTemplateConfig = + /** Template to launch an AWS EC2 instance */ + | { type: "Aws", params: AwsServerTemplateConfig }; + +export type ServerTemplate = Resource; + +export type GetServerTemplateResponse = ServerTemplate; + +export interface ServerTemplateListItemInfo { + /** The cloud provider */ + provider: string; + /** The instance type, eg c5.2xlarge on for Aws templates */ + instance_type?: string; +} + +export type ServerTemplateListItem = ResourceListItem; + +export type ListServerTemplatesResponse = ServerTemplateListItem[]; + +export interface Tag { + /** + * The Mongo ID of the tag. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized Tag) }` + */ + _id?: MongoId; + name: string; + owner?: string; +} + +export type GetTagResponse = Tag; + +export type ListTagsResponse = Tag[]; + +/** Response containing pretty formatted toml contents. */ +export interface TomlResponse { + toml: string; +} + +export type ExportAllResourcesToTomlResponse = TomlResponse; + +export type ExportResourcesToTomlResponse = TomlResponse; + +export enum Operation { + None = "None", + CreateServer = "CreateServer", + UpdateServer = "UpdateServer", + DeleteServer = "DeleteServer", + RenameServer = "RenameServer", + PruneImagesServer = "PruneImagesServer", + PruneContainersServer = "PruneContainersServer", + PruneNetworksServer = "PruneNetworksServer", + CreateNetwork = "CreateNetwork", + DeleteNetwork = "DeleteNetwork", + CreateBuild = "CreateBuild", + UpdateBuild = "UpdateBuild", + DeleteBuild = "DeleteBuild", + RunBuild = "RunBuild", + CancelBuild = "CancelBuild", + CreateBuilder = "CreateBuilder", + UpdateBuilder = "UpdateBuilder", + DeleteBuilder = "DeleteBuilder", + CreateDeployment = "CreateDeployment", + UpdateDeployment = "UpdateDeployment", + DeleteDeployment = "DeleteDeployment", + DeployContainer = "DeployContainer", + StopContainer = "StopContainer", + StopAllContainers = "StopAllContainers", + StartContainer = "StartContainer", + RemoveContainer = "RemoveContainer", + RenameDeployment = "RenameDeployment", + CreateRepo = "CreateRepo", + UpdateRepo = "UpdateRepo", + DeleteRepo = "DeleteRepo", + CloneRepo = "CloneRepo", + PullRepo = "PullRepo", + CreateAlerter = "CreateAlerter", + UpdateAlerter = "UpdateAlerter", + DeleteAlerter = "DeleteAlerter", + CreateProcedure = "CreateProcedure", + UpdateProcedure = "UpdateProcedure", + DeleteProcedure = "DeleteProcedure", + RunProcedure = "RunProcedure", + CreateServerTemplate = "CreateServerTemplate", + UpdateServerTemplate = "UpdateServerTemplate", + DeleteServerTemplate = "DeleteServerTemplate", + LaunchServer = "LaunchServer", +} + +export enum UpdateStatus { + Queued = "Queued", + InProgress = "InProgress", + Complete = "Complete", +} + +export interface Update { + /** + * The Mongo ID of the update. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized Update) }` + */ + _id?: MongoId; + operation: Operation; + start_ts: I64; + success: boolean; + operator: string; + target: ResourceTarget; + logs: Log[]; + end_ts?: I64; + status: UpdateStatus; + version: Version; +} + +export type GetUpdateResponse = Update; + +/** An api key used to authenticate requests via request headers. */ +export interface ApiKey { + /** Unique key associated with secret */ + key: string; + /** Hash of the secret */ + secret: string; + /** User associated with the api key */ + user_id: string; + /** Name associated with the api key for management */ + name: string; + /** Timestamp of key creation */ + created_at: I64; + /** Expiry of key, or 0 if never expires */ + expires: I64; +} + +export type ListApiKeysResponse = ApiKey[]; + +export type ListApiKeysForServiceUserResponse = ApiKey[]; + +export type ListUsersResponse = User[]; + +export interface UserGroup { + /** + * The Mongo ID of the UserGroup. + * This field is de/serialized from/to JSON as + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + */ + _id?: MongoId; + name: string; + /** User ids */ + users: string[]; + updated_at?: I64; +} + +export type GetUserGroupResponse = UserGroup; + +export type ListUserGroupsResponse = UserGroup[]; + +export type DeleteApiKeyResponse = NoData; + +/** Response for [CreateApiKey]. */ +export interface CreateApiKeyResponse { + /** X-API-KEY */ + key: string; + /** + * X-API-SECRET + * + * Note. + * There is no way to get the secret again after it is distributed in this message + */ + secret: string; +} + +export type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; + +export type DeleteApiKeyForServiceUserResponse = NoData; + +export type UpdateDescriptionResponse = NoData; + +export type UpdatePermissionOnTargetResponse = NoData; + +export type UpdateUserBasePermissionsResponse = NoData; + +export type CreateProcedureResponse = Procedure; + +export type CopyProcedureResponse = Procedure; + +export type DeleteProcedureResponse = Procedure; + +export type UpdateProcedureResponse = Procedure; + +export type UpdateTagsOnResourceResponse = NoData; + +export type PushRecentlyViewedResponse = NoData; + +export type SetLastSeenUpdateResponse = NoData; + +export type CreateServiceUserResponse = User; + +export type UpdateServiceUserDescriptionResponse = User; + +export type _PartialCustomAlerterConfig = Partial; + +export type _PartialSlackAlerterConfig = Partial; + +export enum TagBehavior { + /** Returns resources which have strictly all the tags */ + All = "All", + /** Returns resources which have one or more of the tags */ + Any = "Any", +} + +/** Passing empty Vec is the same as not filtering by that field */ +export interface ResourceQuery { + ids?: string[]; + names?: string[]; + /** Pass Vec of tag ids or tag names */ + tags?: string[]; + tag_behavior?: TagBehavior; + specific?: T; +} + +export interface AlerterQuerySpecifics { + types: AlerterConfig["type"][]; +} + +export type AlerterQuery = ResourceQuery; + +export type _PartialBuildConfig = Partial; + +export interface BuildQuerySpecifics { + builder_ids?: string[]; + repos?: string[]; + /** + * query for builds last built more recently than this timestamp + * defaults to 0 which is a no op + */ + built_since?: I64; +} + +export type BuildQuery = ResourceQuery; + +export type _PartialBuilderConfig = Partial; + +export type _PartialServerBuilderConfig = Partial; + +export type _PartialAwsBuilderConfig = Partial; + +export interface BuilderQuerySpecifics { +} + +export type BuilderQuery = ResourceQuery; + +export type _PartialDeploymentConfig = Partial; + +export interface DeploymentQuerySpecifics { + server_ids?: string[]; + build_ids?: string[]; +} + +export type DeploymentQuery = ResourceQuery; + +export type U64 = number; + +export type MongoDocument = any; + +export interface __Serror { + error: string; + trace: string[]; +} + +export type _Serror = __Serror; + +export type _PartialProcedureConfig = Partial; + +export interface ProcedureQuerySpecifics { + types: ProcedureType[]; +} + +export type ProcedureQuery = ResourceQuery; + +export type _PartialRepoConfig = Partial; + +export interface RepoQuerySpecifics { + repos: string[]; +} + +export type RepoQuery = ResourceQuery; + +export type _PartialServerConfig = Partial; + +export interface ServerQuerySpecifics { +} + +/** Server-specific query */ +export type ServerQuery = ResourceQuery; + +export type _PartialAwsServerTemplateConfig = Partial; + +export interface ServerTemplateQuerySpecifics { + types: ServerTemplateConfig["type"][]; +} + +export type ServerTemplateQuery = ResourceQuery; + +export type _PartialTag = Partial; + +/** + * Non authenticated route to see the available options + * users have to login to monitor, eg. local auth, github, google. + * Response: [GetLoginOptionsResponse]. + */ +export interface GetLoginOptions { +} + +/** The response for [GetLoginOptions]. */ +export interface GetLoginOptionsResponse { + /** Whether local auth is enabled. */ + local: boolean; + /** Whether github login is enabled. */ + github: boolean; + /** Whether google login is enabled. */ + google: boolean; +} + +/** + * Create a new local user account. Will fail if a user with the + * given username already exists. + * Response: [CreateLocalUserResponse]. + * + * Note. This method is only available if the core api has `local_auth` enabled. + */ +export interface CreateLocalUser { + /** The username for the new user. */ + username: string; + /** + * The password for the new user. + * This cannot be retreived later. + */ + password: string; +} + +/** + * Login as a local user. Will fail if the users credentials don't match + * any local user. + * + * Note. This method is only available if the core api has `local_auth` enabled. + */ +export interface LoginLocalUser { + /** The user's username */ + username: string; + /** The user's password */ + password: string; +} + +/** + * Exchange a single use exchange token (safe for transport in url query) + * for a jwt. + * Response: [ExchangeForJwtResponse]. + */ +export interface ExchangeForJwt { + /** The 'exchange token' */ + token: string; +} + +/** + * Get the user extracted from the request headers. + * Response: [User]. + */ +export interface GetUser { +} + +/** + * Executes the target build. Response: [Update]. + * + * 1. Get a handle to the builder. If using AWS builder, this means starting a builder ec2 instance. + * 2. Clone the repo on the builder. If an `on_clone` commmand is given, it will be executed. + * 3. Execute `docker build {...params}`, where params are determined using the builds configuration. + * 4. If a dockerhub account is attached, the build will be pushed to that account. + */ +export interface RunBuild { + /** Can be build id or name */ + build: string; +} + +/** + * Cancels the target build. + * Only does anything if the build is `building` when called. + * Response: [Update] + */ +export interface CancelBuild { + /** Can be id or name */ + build: string; +} + +/** + * Deploys the container for the target deployment. Response: [Update]. + * + * 1. Pulls the image onto the target server. + * 2. If the container is already running, + * it will be stopped and removed using `docker container rm ${container_name}`. + * 3. The container will be run using `docker run {...params}`, + * where params are determined by the deployment's configuration. + */ +export interface Deploy { + /** Name or id */ + deployment: string; + /** Override the default termination signal specified in the deployment. */ + stop_signal?: TerminationSignal; + /** Override the default termination max time. */ + stop_time?: number; +} + +/** + * Starts the container for the target deployment. Response: [Update] + * + * 1. Runs `docker start ${container_name}`. + */ +export interface StartContainer { + /** Name or id */ + deployment: string; +} + +/** + * Stops the container for the target deployment. Response: [Update] + * + * 1. Runs `docker stop ${container_name}`. + */ +export interface StopContainer { + /** Name or id */ + deployment: string; + /** Override the default termination signal specified in the deployment. */ + signal?: TerminationSignal; + /** Override the default termination max time. */ + time?: number; +} + +/** + * Stops all deployments on the target server. Response: [Update] + * + * 1. Runs [StopContainer] on all deployments on the server concurrently. + */ +export interface StopAllContainers { + /** Name or id */ + server: string; +} + +/** + * Stops and removes the container for the target deployment. + * Reponse: [Update]. + * + * 1. The container is stopped and removed using `docker container rm ${container_name}`. + */ +export interface RemoveContainer { + /** Name or id. */ + deployment: string; + /** Override the default termination signal specified in the deployment. */ + signal?: TerminationSignal; + /** Override the default termination max time. */ + time?: number; +} + +/** Runs the target procedure. Response: [Update] */ +export interface RunProcedure { + /** Id or name */ + procedure: string; +} + +/** + * Clones the target repo. Response: [Update]. + * + * 1. Clones the repo on the target server using `git clone https://{$token?}@github.com/${repo} -b ${branch}`. + * The token will only be used if a github account is specified, + * and must be declared in the periphery configuration on the target server. + * 2. If `on_clone` and `on_pull` are specified, they will be executed. + * `on_clone` will be executed before `on_pull`. + */ +export interface CloneRepo { + /** Id or name */ + repo: string; +} + +/** + * Pulls the target repo. Response: [Update]. + * + * 1. Pulls the repo on the target server using `git pull`. + * 2. If `on_pull` is specified, it will be executed after the pull is complete. + */ +export interface PullRepo { + /** Id or name */ + repo: string; +} + +/** + * Prunes the docker networks on the target server. Response: [Update]. + * + * 1. Runs `docker network prune -f`. + */ +export interface PruneDockerNetworks { + /** Id or name */ + server: string; +} + +/** + * Prunes the docker images on the target server. Response: [Update]. + * + * 1. Runs `docker image prune -a -f`. + */ +export interface PruneDockerImages { + /** Id or name */ + server: string; +} + +/** + * Prunes the docker containers on the target server. Response: [Update]. + * + * 1. Runs `docker container prune -f`. + */ +export interface PruneDockerContainers { + /** Id or name */ + server: string; +} + +/** + * Launch an EC2 instance with the specified config. + * Response: [Update]. + */ +export interface LaunchServer { + /** The name of the created server. */ + name: string; + /** The server template used to define the config. */ + server_template: string; +} + +/** + * Get a paginated list of alerts sorted by timestamp descending. + * Response: [ListAlertsResponse]. + */ +export interface ListAlerts { + /** + * Pass a custom mongo query to filter the alerts. + * + * ## Example JSON + * ``` + * { + * "resolved": "false", + * "level": "CRITICAL", + * "$or": [ + * { + * "target": { + * "type": "Server", + * "id": "6608bf89cb2a12b257ab6c09" + * } + * }, + * { + * "target": { + * "type": "Server", + * "id": "660a5f60b74f90d5dae45fa3" + * } + * } + * ] + * } + * ``` + * This will filter to only include open alerts that have CRITICAL level on those two servers. + */ + query?: MongoDocument; + /** + * Retrieve older results by incrementing the page. + * `page: 0` is default, and returns the most recent results. + */ + page?: U64; +} + +/** Response for [ListAlerts]. */ +export interface ListAlertsResponse { + alerts: Alert[]; + /** + * If more alerts exist, the next page will be given here. + * Otherwise it will be `null` + */ + next_page?: I64; +} + +/** Get an alert: Response: [Alert]. */ +export interface GetAlert { + id: string; +} + +/** Get a specific alerter. Response: [Alerter]. */ +export interface GetAlerter { + /** Id or name */ + alerter: string; +} + +/** List alerters matching optional query. Response: [ListAlertersResponse]. */ +export interface ListAlerters { + /** Structured query to filter alerters. */ + query?: AlerterQuery; +} + +/** + * Gets a summary of data relating to all alerters. + * Response: [GetAlertersSummaryResponse]. + */ +export interface GetAlertersSummary { +} + +/** Response for [GetAlertersSummary]. */ +export interface GetAlertersSummaryResponse { + total: number; +} + +/** Get a specific build. Response: [Build]. */ +export interface GetBuild { + /** Id or name */ + build: string; +} + +/** List builds matching optional query. Response: [ListBuildsResponse]. */ +export interface ListBuilds { + /** optional structured query to filter builds. */ + query?: BuildQuery; +} + +/** Get current action state for the build. Response: [BuildActionState]. */ +export interface GetBuildActionState { + /** Id or name */ + build: string; +} + +/** + * Gets a summary of data relating to all builds. + * Response: [GetBuildsSummaryResponse]. + */ +export interface GetBuildsSummary { +} + +/** Response for [GetBuildsSummary]. */ +export interface GetBuildsSummaryResponse { + /** The total number of builds in monitor. */ + total: number; +} + +/** + * Gets summary and timeseries breakdown of the last months build count / time for charting. + * Response: [GetBuildMonthlyStatsResponse]. + * + * Note. This method is paginated. One page is 30 days of data. + * Query for older pages by incrementing the page, starting at 0. + */ +export interface GetBuildMonthlyStats { + /** + * Query for older data by incrementing the page. + * `page: 0` is the default, and will return the most recent data. + */ + page?: number; +} + +export interface BuildStatsDay { + time: number; + count: number; + ts: number; +} + +/** Response for [GetBuildMonthlyStats]. */ +export interface GetBuildMonthlyStatsResponse { + total_time: number; + total_count: number; + days: BuildStatsDay[]; +} + +/** + * Paginated endpoint for versions of the build that were built in the past and available for deployment, + * sorted by most recent first. + * Response: [GetBuildVersionsResponse]. + */ +export interface GetBuildVersions { + /** Id or name */ + build: string; + /** The page of data. Default is 0, which corrensponds to the most recent versions. */ + page?: number; + /** Filter to only include versions matching this major version. */ + major?: number; + /** Filter to only include versions matching this minor version. */ + minor?: number; + /** Filter to only include versions matching this patch version. */ + patch?: number; +} + +/** + * List the available docker organizations which can be attached to builds. + * Response: [ListDockerOrganizationsResponse]. + */ +export interface ListDockerOrganizations { +} + +/** Get a specific builder by id or name. Response: [Builder]. */ +export interface GetBuilder { + /** Id or name */ + builder: string; +} + +/** List builders matching structured query. Response: [ListBuildersResponse]. */ +export interface ListBuilders { + query?: BuilderQuery; +} + +/** + * Gets a summary of data relating to all builders. + * Response: [GetBuildersSummaryResponse]. + */ +export interface GetBuildersSummary { +} + +/** Response for [GetBuildersSummary]. */ +export interface GetBuildersSummaryResponse { + /** The total number of builders. */ + total: number; +} + +/** + * Get the docker / github accounts which are available for use on the builder. + * Response: [GetBuilderAvailableAccountsResponse]. + * + * Note. Builds using this builder can only use the docker / github accounts available in this response. + */ +export interface GetBuilderAvailableAccounts { + /** Id or name */ + builder: string; +} + +/** The response for [GetBuilderAvailableAccounts]. */ +export interface GetBuilderAvailableAccountsResponse { + github: string[]; + docker: string[]; +} + +/** Get a specific deployment by name or id. Response: [Deployment]. */ +export interface GetDeployment { + /** Id or name */ + deployment: string; +} + +/** + * List deployments matching optional query. + * Response: [ListDeploymentsResponse]. + */ +export interface ListDeployments { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; +} + +/** + * Get the container, including image / status, of the target deployment. + * Response: [GetDeploymentContainerResponse]. + * + * Note. This does not hit the server directly. The status comes from an + * in memory cache on the core, which hits the server periodically + * to keep it up to date. + */ +export interface GetDeploymentContainer { + /** Id or name */ + deployment: string; +} + +/** Response for [GetDeploymentContainer]. */ +export interface GetDeploymentContainerResponse { + state: DockerContainerState; + container?: ContainerSummary; +} + +/** + * Get the deployment log's tail, split by stdout/stderr. + * Response: [Log]. + * + * Note. This call will hit the underlying server directly for most up to date log. + */ +export interface GetLog { + /** Id or name */ + deployment: string; + /** + * The number of lines of the log tail to include. + * Default: 100. + * Max: 5000. + */ + tail: U64; +} + +export enum SearchCombinator { + Or = "Or", + And = "And", +} + +/** + * Search the deployment log's tail using `grep`. All lines go to stdout. + * Response: [Log]. + * + * Note. This call will hit the underlying server directly for most up to date log. + */ +export interface SearchLog { + /** Id or name */ + deployment: string; + /** The terms to search for. */ + terms: string[]; + /** + * When searching for multiple terms, can use `AND` or `OR` combinator. + * + * - `AND`: Only include lines with **all** terms present in that line. + * - `OR`: Include lines that have one or more matches in the terms. + */ + combinator?: SearchCombinator; +} + +/** + * Get the deployment container's stats using `docker stats`. + * Response: [DockerContainerStats]. + * + * Note. This call will hit the underlying server directly for most up to date stats. + */ +export interface GetDeploymentStats { + /** Id or name */ + deployment: string; +} + +/** + * Get current action state for the deployment. + * Response: [DeploymentActionState]. + */ +export interface GetDeploymentActionState { + /** Id or name */ + deployment: string; +} + +/** + * Gets a summary of data relating to all deployments. + * Response: [GetDeploymentsSummaryResponse]. + */ +export interface GetDeploymentsSummary { +} + +/** Response for [GetDeploymentsSummary]. */ +export interface GetDeploymentsSummaryResponse { + total: I64; + running: I64; + stopped: I64; + not_deployed: I64; + unknown: I64; +} + +/** + * Gets a list of existing values used as extra args across other deployments. + * Useful to offer suggestions. Response: [ListCommonExtraArgsResponse] + */ +export interface ListCommonExtraArgs { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; +} + +/** + * Get the version of the core api. + * Response: [GetVersionResponse]. + */ +export interface GetVersion { +} + +/** Response for [GetVersion]. */ +export interface GetVersionResponse { + /** The version of the core api. */ + version: string; +} + +/** + * Get info about the core api. + * Response: [GetCoreInfoResponse]. + */ +export interface GetCoreInfo { +} + +/** Response for [GetCoreInfo]. */ +export interface GetCoreInfoResponse { + /** The title assigned to this core api. */ + title: string; + /** The monitoring interval of this core api. */ + monitoring_interval: Timelength; + /** The github webhook base url to use with github webhooks. */ + github_webhook_base_url: string; +} + +/** + * List permissions for the calling user. + * Does not include any permissions on UserGroups they may be a part of. + * Response: [ListPermissionsResponse] + */ +export interface ListPermissions { +} + +/** + * Gets the calling user's permission level on a specific resource. + * Factors in any UserGroup's permissions they may be a part of. + * Response: [PermissionLevel] + */ +export interface GetPermissionLevel { + target: ResourceTarget; +} + +/** + * List permissions for a specific user. **Admin only**. + * Response: [ListUserTargetPermissionsResponse] + */ +export interface ListUserTargetPermissions { + /** Specify either a user or a user group. */ + user_target: UserTarget; +} + +/** Get a specific procedure. Response: [Procedure]. */ +export interface GetProcedure { + /** Id or name */ + procedure: string; +} + +/** List procedures matching optional query. Response: [ListProceduresResponse]. */ +export interface ListProcedures { + /** optional structured query to filter procedures. */ + query?: ProcedureQuery; +} + +/** Get current action state for the procedure. Response: [ProcedureActionState]. */ +export interface GetProcedureActionState { + /** Id or name */ + procedure: string; +} + +/** + * Gets a summary of data relating to all procedures. + * Response: [GetProceduresSummaryResponse]. + */ +export interface GetProceduresSummary { +} + +/** Response for [GetProceduresSummary]. */ +export interface GetProceduresSummaryResponse { + total: number; +} + +/** Get a specific repo. Response: [Repo]. */ +export interface GetRepo { + /** Id or name */ + repo: string; +} + +/** List repos matching optional query. Response: [ListReposResponse]. */ +export interface ListRepos { + /** optional structured query to filter repos. */ + query?: RepoQuery; +} + +/** Get current action state for the repo. Response: [RepoActionState]. */ +export interface GetRepoActionState { + /** Id or name */ + repo: string; +} + +/** + * Gets a summary of data relating to all repos. + * Response: [GetReposSummaryResponse]. + */ +export interface GetReposSummary { +} + +/** Response for [GetReposSummary] */ +export interface GetReposSummaryResponse { + total: number; +} + +/** Find resources matching a common query. Response: [FindResourcesResponse]. */ +export interface FindResources { + /** The mongo query as JSON */ + query?: MongoDocument; + /** The resource variants to include in the response. */ + resources?: ResourceTarget["type"][]; +} + +/** Response for [FindResources]. */ +export interface FindResourcesResponse { + /** The matching servers. */ + servers: ServerListItem[]; + /** The matching deployments. */ + deployments: DeploymentListItem[]; + /** The matching builds. */ + builds: BuildListItem[]; + /** The matching repos. */ + repos: RepoListItem[]; + /** The matching procedures. */ + procedures: ProcedureListItem[]; +} + +/** Get a specific server. Response: [Server]. */ +export interface GetServer { + /** Id or name */ + server: string; +} + +/** List servers matching optional query. Response: [ListServersResponse]. */ +export interface ListServers { + /** optional structured query to filter servers. */ + query?: ServerQuery; +} + +/** Get the status of the target server. Response: [GetServerStatusResponse]. */ +export interface GetServerStatus { + /** Id or name */ + server: string; +} + +/** The status for [GetServerStatus]. */ +export interface GetServerStatusResponse { + /** The server status. */ + status: ServerStatus; +} + +/** Get current action state for the servers. Response: [ServerActionState]. */ +export interface GetServerActionState { + /** Id or name */ + server: string; +} + +/** + * Get the version of the monitor periphery agent on the target server. + * Response: [GetPeripheryVersionResponse]. + */ +export interface GetPeripheryVersion { + /** Id or name */ + server: string; +} + +/** Response for [GetPeripheryVersion]. */ +export interface GetPeripheryVersionResponse { + /** The version of periphery. */ + version: string; +} + +/** Get the docker networks on the server. Response: [GetDockerNetworksResponse]. */ +export interface GetDockerNetworks { + /** Id or name */ + server: string; +} + +/** + * Get the docker images locally cached on the target server. + * Response: [GetDockerImagesResponse]. + */ +export interface GetDockerImages { + /** Id or name */ + server: string; +} + +/** + * Get all docker containers on the target server. + * Response: [GetDockerContainersResponse]. + */ +export interface GetDockerContainers { + /** Id or name */ + server: string; +} + +/** + * Get the system information of the target server. + * Response: [SystemInformation]. + */ +export interface GetSystemInformation { + /** Id or name */ + server: string; +} + +/** + * Get the system stats on the target server. Response: [SystemStats]. + * + * Note. This does not hit the server directly. The stats come from an + * in memory cache on the core, which hits the server periodically + * to keep it up to date. + */ +export interface GetSystemStats { + /** Id or name */ + server: string; +} + +/** + * Get the processes running on the target server. + * Response: [GetSystemProcessesResponse]. + * + * Note. This does not hit the server directly. The procedures come from an + * in memory cache on the core, which hits the server periodically + * to keep it up to date. + */ +export interface GetSystemProcesses { + /** Id or name */ + server: string; +} + +/** + * Paginated endpoint serving historical (timeseries) server stats for graphing. + * Response: [GetHistoricalServerStatsResponse]. + */ +export interface GetHistoricalServerStats { + /** Id or name */ + server: string; + /** The granularity of the data. */ + granularity: Timelength; + /** + * Page of historical data. Default is 0, which is the most recent data. + * Use with the `next_page` field of the response. + */ + page?: number; +} + +/** System stats stored on the database. */ +export interface SystemStatsRecord { + /** Unix timestamp in milliseconds */ + ts: I64; + /** Server id */ + sid: string; + /** Cpu usage percentage */ + cpu_perc: number; + /** Memory used in GB */ + mem_used_gb: number; + /** Total memory in GB */ + mem_total_gb: number; + /** Disk used in GB */ + disk_used_gb: number; + /** Total disk size in GB */ + disk_total_gb: number; + /** Breakdown of individual disks, ie their usages, sizes, and mount points */ + disks: SingleDiskUsage[]; +} + +/** Response to [GetHistoricalServerStats]. */ +export interface GetHistoricalServerStatsResponse { + /** The timeseries page of data. */ + stats: SystemStatsRecord[]; + /** If there is a next page of data, pass this to `page` to get it. */ + next_page?: number; +} + +/** + * Gets a summary of data relating to all servers. + * Response: [GetServersSummaryResponse]. + */ +export interface GetServersSummary { +} + +/** Response for [GetServersSummary]. */ +export interface GetServersSummaryResponse { + /** The total number of servers. */ + total: I64; + /** The number of healthy (`status: OK`) servers. */ + healthy: I64; + /** The number of unhealthy servers. */ + unhealthy: I64; + /** The number of disabled servers. */ + disabled: I64; +} + +/** + * Get the usernames for the available github / docker accounts + * on the target server. + * Response: [GetAvailableAccountsResponse]. + */ +export interface GetAvailableAccounts { + /** Id or name */ + server: string; +} + +/** Response for [GetAvailableAccounts]. */ +export interface GetAvailableAccountsResponse { + /** The github usernames */ + github: string[]; + /** The docker usernames. */ + docker: string[]; +} + +/** + * Get the keys for available secrets on the target server. + * Response: [GetAvailableSecretsResponse]. + */ +export interface GetAvailableSecrets { + /** Id or name */ + server: string; +} + +/** Get a specific server template by id or name. Response: [ServerTemplate]. */ +export interface GetServerTemplate { + /** Id or name */ + server_template: string; +} + +/** List server templates matching structured query. Response: [ListServerTemplatesResponse]. */ +export interface ListServerTemplates { + query?: ServerTemplateQuery; +} + +/** + * Gets a summary of data relating to all server templates. + * Response: [GetServerTemplatesSummaryResponse]. + */ +export interface GetServerTemplatesSummary { +} + +/** Response for [GetServerTemplatesSummary]. */ +export interface GetServerTemplatesSummaryResponse { + /** The total number of server templates. */ + total: number; +} + +/** Get data for a specific tag. Response [Tag]. */ +export interface GetTag { + /** Id or name */ + tag: string; +} + +/** + * List data for tags matching optional mongo query. + * Response: [ListTagsResponse]. + */ +export interface ListTags { + query?: MongoDocument; +} + +/** + * Get pretty formatted monrun sync toml for all resources + * which the user has permissions to view. + * Response: [TomlResponse]. + */ +export interface ExportAllResourcesToToml { +} + +/** + * Get pretty formatted monrun sync toml for specific resources and user groups. + * Response: [TomlResponse]. + */ +export interface ExportResourcesToToml { + /** The targets to include in the export. */ + targets: ResourceTarget[]; + /** The user group names or ids to include in the export. */ + user_groups: string[]; +} + +/** + * Get all data for the target update. + * Response: [Update]. + */ +export interface GetUpdate { + id: string; +} + +/** + * Paginated endpoint for updates matching optional query. + * More recent updates will be returned first. + */ +export interface ListUpdates { + /** An optional mongo query to filter the updates. */ + query?: MongoDocument; + /** + * Page of updates. Default is 0, which is the most recent data. + * Use with the `next_page` field of the response. + */ + page?: number; +} + +export interface UpdateListItem { + id: string; + operation: Operation; + start_ts: I64; + success: boolean; + username: string; + operator: string; + target: ResourceTarget; + status: UpdateStatus; + version: Version; +} + +/** Response for [ListUpdates]. */ +export interface ListUpdatesResponse { + /** The page of updates, sorted by timestamp descending. */ + updates: UpdateListItem[]; + /** If there is a next page of data, pass this to `page` to get it. */ + next_page?: number; +} + +/** + * Gets list of api keys for the calling user. + * Response: [ListApiKeysResponse] + */ +export interface ListApiKeys { +} + +/** + * Gets list of api keys for the user at ID. + * **Admin only.** + * Will still fail if you call for a user_id that isn't a service user. + * Response: [ListApiKeysForServiceUserResponse] + */ +export interface ListApiKeysForServiceUser { + /** The id of the user. */ + user_id: string; +} + +/** + * Gets list of monitor users. + * **Admin only.** + * Response: [ListUsersResponse] + */ +export interface ListUsers { +} + +/** + * Gets the username of a specific user. + * Response: [GetUsernameResponse] + */ +export interface GetUsername { + /** The id of the user. */ + user_id: string; +} + +/** Response for [GetUsername]. */ +export interface GetUsernameResponse { + /** The username of the user. */ + username: string; +} + +/** + * Get a specific user group by name or id. + * Response: [UserGroup]. + */ +export interface GetUserGroup { + /** Name or Id */ + user_group: string; +} + +/** + * List all user groups which user can see. Response: [ListUserGroupsResponse]. + * + * Admins can see all user groups, + * and users can see user groups to which they belong. + */ +export interface ListUserGroups { +} + +export type PartialAlerterConfig = + | { type: "Custom", params: _PartialCustomAlerterConfig } + | { type: "Slack", params: _PartialSlackAlerterConfig }; + +/** Create an alerter. Response: [Alerter]. */ +export interface CreateAlerter { + /** The name given to newly created alerter. */ + name: string; + /** Optional partial config to initialize the alerter with. */ + config: PartialAlerterConfig; +} + +/** + * Creates a new alerter with given `name` and the configuration + * of the alerter at the given `id`. Response: [Alerter]. + */ +export interface CopyAlerter { + /** The name of the new alerter. */ + name: string; + /** The id of the alerter to copy. */ + id: string; +} + +/** + * Deletes the alerter at the given id, and returns the deleted alerter. + * Response: [Alerter] + */ +export interface DeleteAlerter { + /** The id of the alerter to delete. */ + id: string; +} + +/** + * Update the alerter at the given id, and return the updated alerter. Response: [Alerter]. + * + * Note. This method updates only the fields which are set in the [PartialAlerterConfig], + * effectively merging diffs into the final document. This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateAlerter { + /** The id of the alerter to update. */ + id: string; + /** The partial config update to apply. */ + config: PartialAlerterConfig; +} + +/** + * Create an api key for the calling user. + * Response: [CreateApiKeyResponse]. + * + * Note. After the response is served, there will be no way + * to get the secret later. + */ +export interface CreateApiKey { + /** The name for the api key. */ + name: string; + /** + * A unix timestamp in millseconds specifying api key expire time. + * Default is 0, which means no expiry. + */ + expires?: I64; +} + +/** + * Delete an api key for the calling user. + * Response: [NoData] + */ +export interface DeleteApiKey { + /** The key which the user intends to delete. */ + key: string; +} + +/** + * Admin only method to create an api key for a service user. + * Response: [CreateApiKeyResponse]. + */ +export interface CreateApiKeyForServiceUser { + /** Must be service user */ + user_id: string; + /** The name for the api key */ + name: string; + /** + * A unix timestamp in millseconds specifying api key expire time. + * Default is 0, which means no expiry. + */ + expires?: I64; +} + +/** + * Admin only method to delete an api key for a service user. + * Response: [NoData]. + */ +export interface DeleteApiKeyForServiceUser { + key: string; +} + +/** Create a build. Response: [Build]. */ +export interface CreateBuild { + /** The name given to newly created build. */ + name: string; + /** Optional partial config to initialize the build with. */ + config: _PartialBuildConfig; +} + +/** + * Creates a new build with given `name` and the configuration + * of the build at the given `id`. Response: [Build]. + */ +export interface CopyBuild { + /** The name of the new build. */ + name: string; + /** The id of the build to copy. */ + id: string; +} + +/** + * Deletes the build at the given id, and returns the deleted build. + * Response: [Build] + */ +export interface DeleteBuild { + /** The id of the build to delete. */ + id: string; +} + +/** + * Update the build at the given id, and return the updated build. + * Response: [Build]. + * + * Note. This method updates only the fields which are set in the [_PartialBuildConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateBuild { + /** The id of the build to update. */ + id: string; + /** The partial config update to apply. */ + config: _PartialBuildConfig; +} + +/** Partial representation of [BuilderConfig] */ +export type PartialBuilderConfig = + | { type: "Server", params: _PartialServerBuilderConfig } + | { type: "Aws", params: _PartialAwsBuilderConfig }; + +/** Create a builder. Response: [Builder]. */ +export interface CreateBuilder { + /** The name given to newly created builder. */ + name: string; + /** Optional partial config to initialize the builder with. */ + config: PartialBuilderConfig; +} + +/** + * Creates a new builder with given `name` and the configuration + * of the builder at the given `id`. Response: [Builder] + */ +export interface CopyBuilder { + /** The name of the new builder. */ + name: string; + /** The id of the builder to copy. */ + id: string; +} + +/** + * Deletes the builder at the given id, and returns the deleted builder. + * Response: [Builder] + */ +export interface DeleteBuilder { + /** The id of the builder to delete. */ + id: string; +} + +/** + * Update the builder at the given id, and return the updated builder. + * Response: [Builder]. + * + * Note. This method updates only the fields which are set in the [PartialBuilderConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateBuilder { + /** The id of the builder to update. */ + id: string; + /** The partial config update to apply. */ + config: PartialBuilderConfig; +} + +/** Create a deployment. Response: [Deployment]. */ +export interface CreateDeployment { + /** The name given to newly created deployment. */ + name: string; + /** Optional partial config to initialize the deployment with. */ + config: _PartialDeploymentConfig; +} + +/** + * Creates a new deployment with given `name` and the configuration + * of the deployment at the given `id`. Response: [Deployment] + */ +export interface CopyDeployment { + /** The name of the new deployment. */ + name: string; + /** The id of the deployment to copy. */ + id: string; +} + +/** + * Deletes the deployment at the given id, and returns the deleted deployment. + * Response: [Deployment]. + */ +export interface DeleteDeployment { + /** The id of the deployment to delete. */ + id: string; +} + +/** + * Update the deployment at the given id, and return the updated deployment. + * Response: [Deployment]. + * + * Note. This method updates only the fields which are set in the [_PartialDeploymentConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateDeployment { + id: string; + config: _PartialDeploymentConfig; +} + +/** + * Rename the deployment at id to the given name. Response: [Update]. + * + * Note. If a container is created for the deployment, it will be renamed using + * `docker rename ...`. + */ +export interface RenameDeployment { + /** The id of the deployment to rename. */ + id: string; + /** The new name. */ + name: string; +} + +/** + * Update a resources description. + * Response: [NoData]. + */ +export interface UpdateDescription { + target: ResourceTarget; + description: string; +} + +/** + * Launch an EC2 instance with the specified config. + * Response: [Update]. + */ +export interface LaunchServer { + /** The name of the created server. */ + name: string; + /** The server template used to define the config. */ + server_template: string; +} + +/** + * Update a user or user groups permission on a resource. + * Response: [NoData]. + */ +export interface UpdatePermissionOnTarget { + /** Specify the user or user group. */ + user_target: UserTarget; + /** Specify the target resource. */ + resource_target: ResourceTarget; + /** Specify the permission level. */ + permission: PermissionLevel; +} + +/** + * Update a user's "base" permissions, eg. "enabled". + * Response: [NoData]. + */ +export interface UpdateUserBasePermissions { + /** The target user. */ + user_id: string; + /** If specified, will update users enabled state. */ + enabled?: boolean; + /** If specified, will update user's ability to create servers. */ + create_servers?: boolean; + /** If specified, will update user's ability to create builds. */ + create_builds?: boolean; +} + +/** Create a procedure. Response: [Procedure]. */ +export interface CreateProcedure { + /** The name given to newly created build. */ + name: string; + /** Optional partial config to initialize the procedure with. */ + config: _PartialProcedureConfig; +} + +/** + * Creates a new procedure with given `name` and the configuration + * of the procedure at the given `id`. Response: [Procedure]. + */ +export interface CopyProcedure { + /** The name of the new procedure. */ + name: string; + /** The id of the procedure to copy. */ + id: string; +} + +/** + * Deletes the procedure at the given id, and returns the deleted procedure. + * Response: [Procedure] + */ +export interface DeleteProcedure { + /** The id of the procedure to delete. */ + id: string; +} + +/** + * Update the procedure at the given id, and return the updated procedure. + * Response: [Procedure]. + * + * Note. This method updates only the fields which are set in the [_PartialProcedureConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateProcedure { + id: string; + config: _PartialProcedureConfig; +} + +/** Create a repo. Response: [Repo]. */ +export interface CreateRepo { + /** The name given to newly created repo. */ + name: string; + /** Optional partial config to initialize the repo with. */ + config: _PartialRepoConfig; +} + +/** + * Creates a new repo with given `name` and the configuration + * of the repo at the given `id`. Response: [Repo]. + */ +export interface CopyRepo { + /** The name of the new repo. */ + name: string; + /** The id of the repo to copy. */ + id: string; +} + +/** + * Deletes the repo at the given id, and returns the deleted repo. + * Response: [Repo] + */ +export interface DeleteRepo { + /** The id of the repo to delete. */ + id: string; +} + +/** + * Update the repo at the given id, and return the updated repo. + * Response: [Repo]. + * + * Note. If the attached server for the repo changes, + * the repo will be deleted / cleaned up on the old server. + * + * Note. This method updates only the fields which are set in the [_PartialRepoConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateRepo { + /** The id of the repo to update. */ + id: string; + /** The partial config update to apply. */ + config: _PartialRepoConfig; +} + +/** Create a server. Response: [Server]. */ +export interface CreateServer { + /** The name given to newly created server. */ + name: string; + /** Optional partial config to initialize the server with. */ + config: _PartialServerConfig; +} + +/** + * Deletes the server at the given id, and returns the deleted server. + * Response: [Server] + */ +export interface DeleteServer { + /** The id of the server to delete. */ + id: string; +} + +/** + * Update the server at the given id, and return the updated server. + * Response: [Server]. + * + * Note. This method updates only the fields which are set in the [_PartialServerConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateServer { + /** The id of the server to update. */ + id: string; + /** The partial config update to apply. */ + config: _PartialServerConfig; +} + +/** Rename the server at id to the given name. Response: [Update]. */ +export interface RenameServer { + /** The id of the server to rename. */ + id: string; + /** The new name. */ + name: string; +} + +/** + * Create a docker network on the server. + * Respone: [Update] + */ +export interface CreateNetwork { + /** Id or name */ + server: string; + /** The name of the network to create. */ + name: string; +} + +/** + * Delete a docker network. + * Response: [Update] + */ +export interface DeleteNetwork { + /** Id or name. */ + server: string; + /** The name of the network to delete. */ + name: string; +} + +export type PartialServerTemplateConfig = + | { type: "Aws", params: _PartialAwsServerTemplateConfig }; + +/** Create a server template. Response: [ServerTemplate]. */ +export interface CreateServerTemplate { + /** The name given to newly created server template. */ + name: string; + /** Optional partial config to initialize the server template with. */ + config: PartialServerTemplateConfig; +} + +/** + * Creates a new server template with given `name` and the configuration + * of the server template at the given `id`. Response: [ServerTemplate] + */ +export interface CopyServerTemplate { + /** The name of the new server template. */ + name: string; + /** The id of the server template to copy. */ + id: string; +} + +/** + * Deletes the server template at the given id, and returns the deleted server template. + * Response: [ServerTemplate] + */ +export interface DeleteServerTemplate { + /** The id of the server template to delete. */ + id: string; +} + +/** + * Update the server template at the given id, and return the updated server template. + * Response: [ServerTemplate]. + * + * Note. This method updates only the fields which are set in the [PartialServerTemplateConfig], + * effectively merging diffs into the final document. + * This is helpful when multiple users are using + * the same resources concurrently by ensuring no unintentional + * field changes occur from out of date local state. + */ +export interface UpdateServerTemplate { + /** The id of the server template to update. */ + id: string; + /** The partial config update to apply. */ + config: PartialServerTemplateConfig; +} + +/** Create a tag. Response: [Tag]. */ +export interface CreateTag { + /** The name of the tag. */ + name: string; +} + +/** + * Delete a tag, and return the deleted tag. Response: [Tag]. + * + * Note. Will also remove this tag from all attached resources. + */ +export interface DeleteTag { + /** The id of the tag to delete. */ + id: string; +} + +/** Rename a tag at id. Response: [Tag]. */ +export interface RenameTag { + /** The id of the tag to rename. */ + id: string; + /** The new name of the tag. */ + name: string; +} + +/** + * Update the tags on a resource. + * Response: [NoData] + */ +export interface UpdateTagsOnResource { + target: ResourceTarget; + /** Tag Ids */ + tags: string[]; +} + +/** + * Push a resource to the front of the users 10 most recently viewed resources. + * Response: [NoData]. + */ +export interface PushRecentlyViewed { + /** The target to push. */ + resource: ResourceTarget; +} + +/** + * Set the time the user last opened the UI updates. + * Used for unseen notification dot. + * Response: [NoData] + */ +export interface SetLastSeenUpdate { +} + +/** + * **Admin only.** Create a service user. + * Response: [User]. + */ +export interface CreateServiceUser { + /** The username for the service user. */ + username: string; + /** A description for the service user. */ + description: string; +} + +/** + * **Admin only.** Update a service user's description. + * Response: [User]. + */ +export interface UpdateServiceUserDescription { + /** The service user's username */ + username: string; + /** A new description for the service user. */ + description: string; +} + +/** **Admin only.** Create a user group. Response: [UserGroup] */ +export interface CreateUserGroup { + /** The name to assign to the new UserGroup */ + name: string; +} + +/** **Admin only.** Rename a user group. Response: [UserGroup] */ +export interface RenameUserGroup { + /** The id of the UserGroup */ + id: string; + /** The new name for the UserGroup */ + name: string; +} + +/** **Admin only.** Delete a user group. Response: [UserGroup] */ +export interface DeleteUserGroup { + /** The id of the UserGroup */ + id: string; +} + +/** **Admin only.** Add a user to a user group. Response: [UserGroup] */ +export interface AddUserToUserGroup { + /** The name or id of UserGroup that user should be added to. */ + user_group: string; + /** The id or username of the user to add */ + user: string; +} + +/** **Admin only.** Remove a user from a user group. Response: [UserGroup] */ +export interface RemoveUserFromUserGroup { + /** The name or id of UserGroup that user should be removed from. */ + user_group: string; + /** The id or username of the user to remove */ + user: string; +} + +/** + * **Admin only.** Completely override the user in the group. + * Response: [UserGroup] + */ +export interface SetUsersInUserGroup { + /** Id or name. */ + user_group: string; + /** The user ids or usernames to hard set as the group's users. */ + users: string[]; +} + +/** Configuration for a custom alerter. */ +export interface CustomAlerterConfig { + /** The http/s endpoint to send the POST to */ + url: string; + /** Whether the alerter is enabled */ + enabled?: boolean; +} + +/** Configuration for a slack alerter. */ +export interface SlackAlerterConfig { + /** The slack app url */ + url: string; + /** Whether the alerter is enabled */ + enabled?: boolean; +} + +/** Configuration for a monitor server builder. */ +export interface ServerBuilderConfig { + /** The server id of the builder */ + server_id: string; +} + +/** Configuration for an AWS builder. */ +export interface AwsBuilderConfig { + /** The AWS region to create the instance in */ + region: string; + /** The instance type to create for the build */ + instance_type: string; + /** The size of the builder volume in gb */ + volume_gb: number; + /** + * The port periphery will be running on. + * Default: `8120` + */ + port: number; + /** + * The EC2 ami id to create. + * The ami should have the periphery client configured to start on startup, + * and should have the necessary github / dockerhub accounts configured. + */ + ami_id: string; + /** The subnet id to create the instance in. */ + subnet_id: string; + /** + * The security group ids to attach to the instance. + * This should include a security group to allow core inbound access to the periphery port. + */ + security_group_ids: string[]; + /** The key pair name to attach to the instance */ + key_pair_name: string; + /** + * Whether to assign the instance a public IP address. + * Likely needed for the instance to be able to reach the open internet. + */ + assign_public_ip: boolean; + /** + * Whether core should use the public IP address to communicate with periphery on the builder. + * If false, core will communicate with the instance using the private IP. + */ + use_public_ip: boolean; + /** Which github accounts (usernames) are available on the AMI */ + github_accounts?: string[]; + /** Which dockerhub accounts (usernames) are available on the AMI */ + docker_accounts?: string[]; +} + +export interface CloneArgs { + name: string; + repo?: string; + branch?: string; + on_clone?: SystemCommand; + on_pull?: SystemCommand; + github_account?: string; +} + +/** Info for the all system disks combined. */ +export interface TotalDiskUsage { + /** Used portion in GB */ + used_gb: number; + /** Total size in GB */ + total_gb: number; +} + +/** Summary of the health of the server. */ +export interface ServerHealth { + cpu: SeverityLevel; + mem: SeverityLevel; + disks: Record; +} + +/** + * For information on AWS volumes, see + * ``. + */ +export interface AwsVolume { + /** The device name (for example, `/dev/sda1` or `xvdh`). */ + device_name: string; + /** The size of the volume in GB */ + size_gb: number; + /** The type of volume, eg gp2, gp3, io1 */ + volume_type?: string; + /** The iops of the volume, or AWS default. */ + iops?: number; + /** The throughput of the volume, or AWS default. */ + throughput?: number; +} + +/** Aws EC2 instance config. */ +export interface AwsServerTemplateConfig { + /** The aws region to launch the server in, eg. us-east-1 */ + region: string; + /** The instance type to launch, eg. c5.2xlarge */ + instance_type: string; + /** Specify the EBS volumes to attach. */ + volumes: AwsVolume[]; + /** Specify the ami id to use. Must be set up to start the periphery binary on startup. */ + ami_id: string; + /** The subnet to assign to the instance. */ + subnet_id: string; + /** The security groups to give to the instance. */ + security_group_ids: string[]; + /** The key pair name to give to the instance in case SSH access required. */ + key_pair_name: string; + /** + * Assign a public ip to the instance. Depending on how your network is + * setup, this may be required for the instance to reach the public internet. + */ + assign_public_ip: boolean; + /** + * Use the instances public ip as the address for the server. + * Could be used when build instances are created in another non-interconnected network to the core api. + */ + use_public_ip: boolean; + /** + * The port periphery will be running on in AMI. + * Default: `8120` + */ + port: number; +} + +export type AuthRequest = + | { type: "GetLoginOptions", params: GetLoginOptions } + | { type: "CreateLocalUser", params: CreateLocalUser } + | { type: "LoginLocalUser", params: LoginLocalUser } + | { type: "ExchangeForJwt", params: ExchangeForJwt } + | { type: "GetUser", params: GetUser }; + +export type ExecuteRequest = + | { type: "PruneContainers", params: PruneDockerContainers } + | { type: "PruneImages", params: PruneDockerImages } + | { type: "PruneNetworks", params: PruneDockerNetworks } + | { type: "Deploy", params: Deploy } + | { type: "StartContainer", params: StartContainer } + | { type: "StopContainer", params: StopContainer } + | { type: "StopAllContainers", params: StopAllContainers } + | { type: "RemoveContainer", params: RemoveContainer } + | { type: "RunBuild", params: RunBuild } + | { type: "CancelBuild", params: CancelBuild } + | { type: "CloneRepo", params: CloneRepo } + | { type: "PullRepo", params: PullRepo } + | { type: "RunProcedure", params: RunProcedure } + | { type: "LaunchServer", params: LaunchServer }; + +export type ReadRequest = + | { type: "GetVersion", params: GetVersion } + | { type: "GetCoreInfo", params: GetCoreInfo } + | { type: "ListUsers", params: ListUsers } + | { type: "GetUsername", params: GetUsername } + | { type: "ListApiKeys", params: ListApiKeys } + | { type: "ListApiKeysForServiceUser", params: ListApiKeysForServiceUser } + | { type: "ListPermissions", params: ListPermissions } + | { type: "GetPermissionLevel", params: GetPermissionLevel } + | { type: "ListUserTargetPermissions", params: ListUserTargetPermissions } + | { type: "GetUserGroup", params: GetUserGroup } + | { type: "ListUserGroups", params: ListUserGroups } + | { type: "FindResources", params: FindResources } + | { type: "GetProceduresSummary", params: GetProceduresSummary } + | { type: "GetProcedure", params: GetProcedure } + | { type: "GetProcedureActionState", params: GetProcedureActionState } + | { type: "ListProcedures", params: ListProcedures } + | { type: "GetServerTemplate", params: GetServerTemplate } + | { type: "ListServerTemplates", params: ListServerTemplates } + | { type: "GetServerTemplateSummary", params: GetServerTemplatesSummary } + | { type: "GetServersSummary", params: GetServersSummary } + | { type: "GetServer", params: GetServer } + | { type: "ListServers", params: ListServers } + | { type: "GetServerStatus", params: GetServerStatus } + | { type: "GetPeripheryVersion", params: GetPeripheryVersion } + | { type: "GetDockerContainers", params: GetDockerContainers } + | { type: "GetDockerImages", params: GetDockerImages } + | { type: "GetDockerNetworks", params: GetDockerNetworks } + | { type: "GetServerActionState", params: GetServerActionState } + | { type: "GetHistoricalServerStats", params: GetHistoricalServerStats } + | { type: "GetAvailableAccounts", params: GetAvailableAccounts } + | { type: "GetAvailableSecrets", params: GetAvailableSecrets } + | { type: "GetDeploymentsSummary", params: GetDeploymentsSummary } + | { type: "GetDeployment", params: GetDeployment } + | { type: "ListDeployments", params: ListDeployments } + | { type: "GetDeploymentContainer", params: GetDeploymentContainer } + | { type: "GetDeploymentActionState", params: GetDeploymentActionState } + | { type: "GetDeploymentStats", params: GetDeploymentStats } + | { type: "GetLog", params: GetLog } + | { type: "SearchLog", params: SearchLog } + | { type: "ListCommonExtraArgs", params: ListCommonExtraArgs } + | { type: "GetBuildsSummary", params: GetBuildsSummary } + | { type: "GetBuild", params: GetBuild } + | { type: "ListBuilds", params: ListBuilds } + | { type: "GetBuildActionState", params: GetBuildActionState } + | { type: "GetBuildMonthlyStats", params: GetBuildMonthlyStats } + | { type: "GetBuildVersions", params: GetBuildVersions } + | { type: "ListDockerOrganizations", params: ListDockerOrganizations } + | { type: "GetReposSummary", params: GetReposSummary } + | { type: "GetRepo", params: GetRepo } + | { type: "ListRepos", params: ListRepos } + | { type: "GetRepoActionState", params: GetRepoActionState } + | { type: "GetBuildersSummary", params: GetBuildersSummary } + | { type: "GetBuilder", params: GetBuilder } + | { type: "ListBuilders", params: ListBuilders } + | { type: "GetBuilderAvailableAccounts", params: GetBuilderAvailableAccounts } + | { type: "GetAlertersSummary", params: GetAlertersSummary } + | { type: "GetAlerter", params: GetAlerter } + | { type: "ListAlerters", params: ListAlerters } + | { type: "ExportAllResourcesToToml", params: ExportAllResourcesToToml } + | { type: "ExportResourcesToToml", params: ExportResourcesToToml } + | { type: "GetTag", params: GetTag } + | { type: "ListTags", params: ListTags } + | { type: "GetUpdate", params: GetUpdate } + | { type: "ListUpdates", params: ListUpdates } + | { type: "ListAlerts", params: ListAlerts } + | { type: "GetAlert", params: GetAlert } + | { type: "GetSystemInformation", params: GetSystemInformation } + | { type: "GetSystemStats", params: GetSystemStats } + | { type: "GetSystemProcesses", params: GetSystemProcesses }; + +export type WriteRequest = + | { type: "CreateApiKey", params: CreateApiKey } + | { type: "DeleteApiKey", params: DeleteApiKey } + | { type: "CreateApiKeyForServiceUser", params: CreateApiKeyForServiceUser } + | { type: "DeleteApiKeyForServiceUser", params: DeleteApiKeyForServiceUser } + | { type: "PushRecentlyViewed", params: PushRecentlyViewed } + | { type: "SetLastSeenUpdate", params: SetLastSeenUpdate } + | { type: "CreateServiceUser", params: CreateServiceUser } + | { type: "UpdateServiceUserDescription", params: UpdateServiceUserDescription } + | { type: "CreateUserGroup", params: CreateUserGroup } + | { type: "RenameUserGroup", params: RenameUserGroup } + | { type: "DeleteUserGroup", params: DeleteUserGroup } + | { type: "AddUserToUserGroup", params: AddUserToUserGroup } + | { type: "RemoveUserFromUserGroup", params: RemoveUserFromUserGroup } + | { type: "SetUsersInUserGroup", params: SetUsersInUserGroup } + | { type: "UpdateUserBasePermissions", params: UpdateUserBasePermissions } + | { type: "UpdatePermissionOnTarget", params: UpdatePermissionOnTarget } + | { type: "UpdateDescription", params: UpdateDescription } + | { type: "CreateServer", params: CreateServer } + | { type: "DeleteServer", params: DeleteServer } + | { type: "UpdateServer", params: UpdateServer } + | { type: "RenameServer", params: RenameServer } + | { type: "CreateNetwork", params: CreateNetwork } + | { type: "DeleteNetwork", params: DeleteNetwork } + | { type: "CreateDeployment", params: CreateDeployment } + | { type: "CopyDeployment", params: CopyDeployment } + | { type: "DeleteDeployment", params: DeleteDeployment } + | { type: "UpdateDeployment", params: UpdateDeployment } + | { type: "RenameDeployment", params: RenameDeployment } + | { type: "CreateBuild", params: CreateBuild } + | { type: "CopyBuild", params: CopyBuild } + | { type: "DeleteBuild", params: DeleteBuild } + | { type: "UpdateBuild", params: UpdateBuild } + | { type: "CreateBuilder", params: CreateBuilder } + | { type: "CopyBuilder", params: CopyBuilder } + | { type: "DeleteBuilder", params: DeleteBuilder } + | { type: "UpdateBuilder", params: UpdateBuilder } + | { type: "CreateServerTemplate", params: CreateServerTemplate } + | { type: "CopyServerTemplate", params: CopyServerTemplate } + | { type: "DeleteServerTemplate", params: DeleteServerTemplate } + | { type: "UpdateServerTemplate", params: UpdateServerTemplate } + | { type: "CreateRepo", params: CreateRepo } + | { type: "CopyRepo", params: CopyRepo } + | { type: "DeleteRepo", params: DeleteRepo } + | { type: "UpdateRepo", params: UpdateRepo } + | { type: "CreateAlerter", params: CreateAlerter } + | { type: "CopyAlerter", params: CopyAlerter } + | { type: "DeleteAlerter", params: DeleteAlerter } + | { type: "UpdateAlerter", params: UpdateAlerter } + | { type: "CreateProcedure", params: CreateProcedure } + | { type: "CopyProcedure", params: CopyProcedure } + | { type: "DeleteProcedure", params: DeleteProcedure } + | { type: "UpdateProcedure", params: UpdateProcedure } + | { type: "CreateTag", params: CreateTag } + | { type: "DeleteTag", params: DeleteTag } + | { type: "RenameTag", params: RenameTag } + | { type: "UpdateTagsOnResource", params: UpdateTagsOnResource }; + +export type WsLoginMessage = + | { type: "Jwt", params: { + jwt: string; +}} + | { type: "ApiKeys", params: { + key: string; + secret: string; +}}; + diff --git a/client/core/ts/tsconfig.json b/client/core/ts/tsconfig.json new file mode 100644 index 000000000..b2162f14e --- /dev/null +++ b/client/core/ts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "outDir": "dist", + "declaration": true + } +} \ No newline at end of file diff --git a/client/core/ts/yarn.lock b/client/core/ts/yarn.lock new file mode 100644 index 000000000..d544f1b5e --- /dev/null +++ b/client/core/ts/yarn.lock @@ -0,0 +1,65 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== diff --git a/client/periphery/rs/Cargo.toml b/client/periphery/rs/Cargo.toml new file mode 100644 index 000000000..6f9d17125 --- /dev/null +++ b/client/periphery/rs/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "periphery_client" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# local +monitor_client.workspace = true +# mogh +serror.workspace = true +resolver_api.workspace = true +# external +reqwest.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true \ No newline at end of file diff --git a/client/periphery/rs/src/api/build.rs b/client/periphery/rs/src/api/build.rs new file mode 100644 index 000000000..f8db3c8e6 --- /dev/null +++ b/client/periphery/rs/src/api/build.rs @@ -0,0 +1,33 @@ +use monitor_client::entities::{ + server::docker_image::ImageSummary, update::Log, +}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(BuildResponse)] +pub struct Build { + pub build: monitor_client::entities::build::Build, + /// Override docker token with one sent from core. + pub docker_token: Option, +} + +pub type BuildResponse = Vec; + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(GetImageListResponse)] +pub struct GetImageList {} + +pub type GetImageListResponse = Vec; + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(PruneImagesResponse)] +pub struct PruneImages {} + +pub type PruneImagesResponse = Log; + +// diff --git a/client/periphery/rs/src/api/container.rs b/client/periphery/rs/src/api/container.rs new file mode 100644 index 000000000..ae552013e --- /dev/null +++ b/client/periphery/rs/src/api/container.rs @@ -0,0 +1,108 @@ +use monitor_client::entities::{ + deployment::{ + ContainerSummary, Deployment, DockerContainerStats, + TerminationSignal, + }, + update::Log, + SearchCombinator, +}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct GetContainerList {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct GetContainerLog { + pub name: String, + #[serde(default = "default_tail")] + pub tail: u64, +} + +fn default_tail() -> u64 { + 50 +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct GetContainerLogSearch { + pub name: String, + pub terms: Vec, + #[serde(default)] + pub combinator: SearchCombinator, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(DockerContainerStats)] +pub struct GetContainerStats { + pub name: String, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct GetContainerStatsList {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct StartContainer { + pub name: String, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct StopContainer { + pub name: String, + pub signal: Option, + pub time: Option, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct RemoveContainer { + pub name: String, + pub signal: Option, + pub time: Option, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct RenameContainer { + pub curr_name: String, + pub new_name: String, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct PruneContainers {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct Deploy { + pub deployment: Deployment, + /// Override docker token with one sent from core. + pub docker_token: Option, + pub stop_signal: Option, + pub stop_time: Option, +} diff --git a/client/periphery/rs/src/api/git.rs b/client/periphery/rs/src/api/git.rs new file mode 100644 index 000000000..fa0e24f9e --- /dev/null +++ b/client/periphery/rs/src/api/git.rs @@ -0,0 +1,31 @@ +use monitor_client::entities::{ + update::Log, CloneArgs, SystemCommand, +}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct CloneRepo { + pub args: CloneArgs, + /// Override github token with one sent from core. + pub github_token: Option, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct PullRepo { + pub name: String, + pub branch: Option, + pub on_pull: Option, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct DeleteRepo { + pub name: String, +} diff --git a/client/periphery/rs/src/api/mod.rs b/client/periphery/rs/src/api/mod.rs new file mode 100644 index 000000000..8ab5074ab --- /dev/null +++ b/client/periphery/rs/src/api/mod.rs @@ -0,0 +1,61 @@ +use monitor_client::entities::{update::Log, SystemCommand}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +pub mod build; +pub mod container; +pub mod git; +pub mod network; +pub mod stats; + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(GetHealthResponse)] +pub struct GetHealth {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetHealthResponse {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(GetVersionResponse)] +pub struct GetVersion {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetVersionResponse { + pub version: String, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(GetAccountsResponse)] +pub struct GetAccounts {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetAccountsResponse { + pub docker: Vec, + pub github: Vec, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct GetSecrets {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct PruneSystem {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct RunCommand { + pub command: SystemCommand, +} diff --git a/client/periphery/rs/src/api/network.rs b/client/periphery/rs/src/api/network.rs new file mode 100644 index 000000000..afec5a0e6 --- /dev/null +++ b/client/periphery/rs/src/api/network.rs @@ -0,0 +1,32 @@ +use monitor_client::entities::{ + server::docker_network::DockerNetwork, update::Log, +}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct GetNetworkList {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct CreateNetwork { + pub name: String, + pub driver: Option, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct DeleteNetwork { + pub name: String, +} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Log)] +pub struct PruneNetworks {} diff --git a/client/periphery/rs/src/api/stats.rs b/client/periphery/rs/src/api/stats.rs new file mode 100644 index 000000000..bd3c7880e --- /dev/null +++ b/client/periphery/rs/src/api/stats.rs @@ -0,0 +1,23 @@ +use monitor_client::entities::server::stats::{ + SystemInformation, SystemProcess, SystemStats, +}; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(SystemInformation)] +pub struct GetSystemInformation {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(SystemStats)] +pub struct GetSystemStats {} + +// + +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(Vec)] +pub struct GetSystemProcesses {} + +// diff --git a/client/periphery/rs/src/lib.rs b/client/periphery/rs/src/lib.rs new file mode 100644 index 000000000..2658f6132 --- /dev/null +++ b/client/periphery/rs/src/lib.rs @@ -0,0 +1,104 @@ +use std::{sync::OnceLock, time::Duration}; + +use anyhow::Context; +use reqwest::StatusCode; +use resolver_api::HasResponse; +use serde_json::json; +use serror::deserialize_error; + +pub mod api; + +fn http_client() -> &'static reqwest::Client { + static PERIPHERY_HTTP_CLIENT: OnceLock = + OnceLock::new(); + PERIPHERY_HTTP_CLIENT.get_or_init(Default::default) +} + +pub struct PeripheryClient { + address: String, + passkey: String, +} + +impl PeripheryClient { + pub fn new( + address: impl Into, + passkey: impl Into, + ) -> PeripheryClient { + PeripheryClient { + address: address.into(), + passkey: passkey.into(), + } + } + + // tracing will skip self, to avoid including passkey in traces + #[tracing::instrument( + name = "PeripheryRequest", + level = "debug", + skip(self) + )] + pub async fn request( + &self, + request: T, + ) -> anyhow::Result { + tracing::debug!("running health check"); + self.health_check().await?; + tracing::debug!("health check passed. running inner request"); + self.request_inner(request, None).await + } + + #[tracing::instrument(level = "debug", skip(self))] + pub async fn health_check(&self) -> anyhow::Result<()> { + self + .request_inner(api::GetHealth {}, Some(Duration::from_secs(1))) + .await?; + Ok(()) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn request_inner( + &self, + request: T, + timeout: Option, + ) -> anyhow::Result { + let req_type = T::req_type(); + tracing::trace!( + "sending request | type: {req_type} | body: {request:?}" + ); + let mut req = http_client() + .post(&self.address) + .json(&json!({ + "type": req_type, + "params": request + })) + .header("authorization", &self.passkey); + if let Some(timeout) = timeout { + req = req.timeout(timeout); + } + let res = + req.send().await.context("failed at request to periphery")?; + let status = res.status(); + tracing::debug!( + "got response | type: {req_type} | {status} | body: {res:?}", + ); + if status == StatusCode::OK { + tracing::debug!("response ok, deserializing"); + res.json().await.with_context(|| format!( + "failed to parse response to json | type: {req_type} | body: {request:?}" + )) + } else { + tracing::debug!("response is non-200"); + + let text = res + .text() + .await + .context("failed to convert response to text")?; + + tracing::debug!("got response text, deserializing error"); + + let error = deserialize_error(text) + .context(format!("request to periphery failed | {status}")); + + Err(error) + } + } +} diff --git a/config_example/core.config.example.toml b/config_example/core.config.example.toml index ec75ae9d9..989b5d937 100644 --- a/config_example/core.config.example.toml +++ b/config_example/core.config.example.toml @@ -1,73 +1,97 @@ -# optional. this will be the document title on the web page (shows up as text in the browser tab). default is 'monitor' -title = "monitor" +## this will be the document title on the web page (shows up as text in the browser tab). +## default: 'Monitor' +title = "Monitor" -# this should be the url used to access monitor in browser, potentially behind DNS, eg https://monitor.mogh.tech or http://12.34.56.78:9000 -host = "https://monitor.mogh.tech" +## required for oauth functionality. this should be the url used to access monitor in browser, potentially behind DNS. +## eg https://monitor.dev or http://12.34.56.78:9000. this should match the address configured in your oauth app. +## no default +host = "https://monitor.dev" -# the port the core system will run on. if running core in docker container, leave as this port as 9000 and use port bind eg. -p 9001:9000 +## the port the core system will run on. if running core in docker container, leave as this port as 9000 and use port bind eg. -p 9001:9000 +## default: 9000 port = 9000 -# daily utc offset in hours to send daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight. -daily_offset_hours = 13 +## required to match a passkey in periphery config. token used to authenticate core requests to periphery +## no default +passkey = "a_random_passkey" -# secret used to generate the jwt. should be some randomly generated hash. -jwt_secret = "your_jwt_secret" +## specify the log level of the monitor core application +## default: info +## options: off, error, warn, info, debug, trace +logging.level = "info" -# can be 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk, 30-day -jwt_valid_for = "1-wk" +## specify the logging format for stdout / stderr. +## default: standard +## options: standard, json, none +logging.stdio = "standard" -# webhook url given by slack app that monitor will send alerts and a daily update to -slack_url = "your_slack_app_webhook_url" +## specify a opentelemetry otlp endpoint to send traces to +## optional, default unassigned +# logging.otlp_endpoint = "http://localhost:4317" -# token that has to be given to github during repo webhook config as the secret +## specify how long an issued jwt stays valid. all jwts are invalidated on application restart. +## default: 1-day. +## options: 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk, 30-day +jwt_valid_for = "1-day" + +## controls the granularity of the system stats collection by monitor core +## default: 15-sec +## options: 5-sec, 15-sec, 30-sec, 1-min, 2-min, 5-min, 15-min +monitoring_interval = "15-sec" + +## number of days to keep stats around, or 0 to disable pruning. +## stats older than this number of days are deleted daily +## default: 0 (pruning disabled) +keep_stats_for_days = 0 + +## token that has to be given to github during repo webhook config as the secret +## default: empty (none) github_webhook_secret = "your_random_webhook_secret" -# optional. an alternate base url that is used to recieve github webhook requests. if not provided, will use 'host' address as base -github_webhook_base_url = "https://monitor-github-webhook.mogh.tech" +## an alternate base url that is used to recieve github webhook requests +## if empty or not specified, will use 'host' address as base +## default: empty (none) +# github_webhook_base_url = "https://github-webhook.monitor.dev" -# token used to authenticate core requests to periphery -passkey = "your_random_passkey" +## these will be used by the GUI to attach to builds. +## when attached to build, image will be pushed to repo under the specified organization. +## if empty, the "docker organization" config option will not be shown. +## default: empty +# docker_organizations = ["your_docker_org1", "your_docker_org_2"] -# controls the granularity of the system stats collection by monitor core -# can be 15-sec, 30-sec, 1-min, 2-min, 5-min -monitoring_interval = "1-min" +## allow or deny user login with username / password +## default: false +# local_auth = true -# number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily -keep_stats_for_days = 14 +# github_oauth.enabled = true +# github_oauth.id = "your_github_client_id" +# github_oauth.secret = "your_github_client_secret" -# these will be used by the GUI to attach to builds. New build docker orgs will default to first org (or none if empty). -# when attached to build, image will be pushed to repo under the specified organization -docker_organizations = ["your_docker_org1", "your_docker_org_2"] +# google_oauth.enabled = true +# google_oauth.id = "your_google_client_id" +# google_oauth.secret = "your_google_client_secret" -# allow or deny user login with username / password -local_auth = true +# mongo.uri = "mongodb://username:password@localhost:27017" +## ==== or ==== +mongo.address = "localhost:27017" +# mongo.username = "username" +# mongo.password = "password" +## ==== other ==== +## default: monitor. this is the name of the mongo database that monitor will create its collections in. +mongo.db_name = "monitor" +## default: monitor_core. this is the assigned app_name of the mongo client +mongo.app_name = "monitor_core" -[github_oauth] -enabled = true -id = "your_github_client_id" -secret = "your_github_client_secret" +## provide aws api keys for ephemeral builders +# aws.access_key_id = "your_aws_key_id" +# aws.secret_access_key = "your_aws_secret_key" -[google_oauth] -enabled = true -id = "your_google_client_id" -secret = "your_google_client_secret" +## provide core-based github accounts +[github_accounts] +# github_username_1 = "github_token_1" +# github_username_2 = "github_token_2" -[mongo] -uri = "your_mongo_uri" -app_name = "monitor_core" -db_name = "monitor" # this is the name of the mongo database that monitor will create its collections in. - -[aws] -access_key_id = "your_aws_key_id" -secret_access_key = "your_aws_secret_key" -default_region = "us-east-1" -default_ami_name = "your_ami_name" # must be defined below in [aws.available_ami_accounts] -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_key_pair_name = "your_default_key_pair_name" -default_assign_public_ip = false - -[aws.available_ami_accounts] -your_ami_name = { ami_id = "ami-1234567890", github = ["github_username"], docker = ["docker_username"] } +## provide core-based docker accounts +[docker_accounts] +# docker_username_1 = "docker_token_1" +# docker_username_2 = "docker_token_2" diff --git a/config_example/core.env.example b/config_example/core.env.example index a0c35be41..a8ceb57a3 100644 --- a/config_example/core.env.example +++ b/config_example/core.env.example @@ -1 +1,48 @@ -CONFIG_PATH=../config/core.config.example.toml # optional, default is /config/config.toml. this is usually bind mounted into the container \ No newline at end of file +# optional. default is /frontend +MONITOR_FRONTEND_PATH=/frontend + +# optional. default is /config/config.toml +MONITOR_CONFIG_PATH=/config/config.toml + +## All config file fields are optionally available to override on environment +## The config fields are prefixed with 'MONITOR_' +## Nested config fields are set by converting nesting to 'MONITOR_{field}', where field is uppercase + +## Note. ALL the following are optional, and could also be specified in the config file +MONITOR_TITLE=Monitor +MONITOR_HOST=https://monitor.dev +MONITOR_PORT=9000 +MONITOR_PASSKEY=asdfasdf +MONITOR_JWT_VALID_FOR=1-day +MONITOR_MONITORING_INTERVAL=15-sec +MONITOR_KEEP_STATS_FOR_DAYS=0 +MONITOR_KEEP_ALERTS_FOR_DAYS=0 +MONITOR_GITHUB_WEBHOOK_SECRET=asdfasdf +MONITOR_GITHUB_WEBHOOK_BASE_URL=https://github-listener.monitor.dev +MONITOR_DOCKER_ORGANIZATIONS=org1,org2 + +MONITOR_LOGGING_LEVEL=info +MONITOR_LOGGING_STDIO=standard +MONITOR_LOGGING_OTLP_ENDPOINT=http://localhost:4317 + +MONITOR_LOCAL_AUTH=true + +MONITOR_GITHUB_OAUTH_ENABLED=true +MONITOR_GITHUB_OAUTH_ID=asdfasdf +MONITOR_GITHUB_OAUTH_SECRET=asdfasdf + +MONITOR_GOOGLE_OAUTH_ENABLED=true +MONITOR_GOOGLE_OAUTH_ID=asdfasdf +MONITOR_GOOGLE_OAUTH_SECRET=asdfasdf + +MONITOR_MONGO_URI=mongodb://admin:admin@localhost:27017 +# or +MONITOR_MONGO_ADDRESS=localhost:27017 +MONITOR_MONGO_USERNAME=admin +MONITOR_MONGO_PASSWORD=admin + +MONITOR_MONGO_APP_NAME=monitor_core +MONITOR_MONGO_DB_NAME=monitor + +MONITOR_AWS_ACCESS_KEY_ID=asdfasdf +MONITOR_AWS_SECRET_ACCESS_KEY=asdfasdf diff --git a/config_example/periphery.config.example.toml b/config_example/periphery.config.example.toml index 7b701c906..bee106d2a 100644 --- a/config_example/periphery.config.example.toml +++ b/config_example/periphery.config.example.toml @@ -1,16 +1,42 @@ -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. +# optional. 8120 is default +port = 8120 -[secrets] # optional. can inject these values into your deployments configuration. +# optional. /repos is default. no reason to change if running the docker container, just mount your desired repo dir to /repos in the container +repo_dir = "/repos" + +# optional. 5-sec is default. can use 1-sec, 5-sec, 10-sec, 30-sec, 1-min. controls granularity of system stats recorded +stats_polling_rate = "5-sec" + +# optional. default is empty, which will not block any request by ip. +allowed_ips = ["127.0.0.1"] + +# optional. default is empty, which will not require any passkey to be passed by core. +passkeys = ["abcdefghijk"] + +# specify the log level of the monitor core application +# default: info +# options: off, error, warn, info, debug, trace +logging.level = "info" + +# specify the logging format for stdout / stderr. +# default: standard +# options: standard, json, none +logging.stdio = "standard" + +# specify a loki endpoint to send tracing logs to +# optional, default unassigned +#logging.loki_url = "http://localhost:3100" + +# optional. can inject these values into your deployments configuration. +[secrets] secret_variable = "secret_value" -[github_accounts] # optional +# optional. +[github_accounts] github_username1 = "github_token1" github_username2 = "github_token2" -[docker_accounts] # optional +# optional. +[docker_accounts] docker_username1 = "docker_token1" docker_username2 = "docker_token2" diff --git a/config_example/periphery.env.example b/config_example/periphery.env.example deleted file mode 100644 index b0e202fdd..000000000 --- a/config_example/periphery.env.example +++ /dev/null @@ -1 +0,0 @@ -CONFIG_PATH=../config/periphery.config.example.toml # optional, default is /config/config.toml. this is usually bind mounted into the container \ No newline at end of file diff --git a/core/Cargo.toml b/core/Cargo.toml deleted file mode 100644 index 540a4134b..000000000 --- a/core/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "core" -version = "0.3.4" -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" } -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.28", features = ["full"] } -tokio-tungstenite = { version = "0.19", features=["native-tls"] } -tokio-util = { version = "0.7"} -axum = { version = "0.6", features = ["ws", "json"] } -tower = { version = "0.4", features = ["timeout"] } -tower-http = { version = "0.4", features = ["fs", "cors"] } -slack = { package = "slack_client_rs", version = "0.0.8" } -futures-util = "0.3" -mungos = "0.3.19" -serde = "1.0" -serde_json = "1.0" -dotenv = "0.15" -envy = "0.4" -anyhow = "1.0" -bcrypt = "0.14" -jwt = "0.16" -hmac = "0.12" -sha2 = "0.10" -async_timing_util = "0.1.14" -diff-struct = "0.5" -typeshare = "1.0.1" -hex = "0.4" -aws-config = "0.55.2" -aws-sdk-ec2 = "0.27.0" -merge_config_files = "0.1.3" -termination_signal = "0.1.2" \ No newline at end of file diff --git a/core/Dockerfile b/core/Dockerfile deleted file mode 100644 index 5d121c49c..000000000 --- a/core/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM rust:latest as builder -WORKDIR /builder - -COPY ./core ./core - -COPY ./lib/types ./lib/types -COPY ./lib/helpers ./lib/helpers - -COPY ./lib/db_client ./lib/db_client -COPY ./lib/periphery_client ./lib/periphery_client -COPY ./lib/axum_oauth2 ./lib/axum_oauth2 - -RUN cd core && cargo build --release - -FROM gcr.io/distroless/cc - -COPY ./frontend/build /frontend - -COPY --from=builder /builder/core/target/release/core / - -EXPOSE 9000 - -CMD ["./core"] \ No newline at end of file diff --git a/core/src/actions/build.rs b/core/src/actions/build.rs deleted file mode 100644 index 35295e886..000000000 --- a/core/src/actions/build.rs +++ /dev/null @@ -1,685 +0,0 @@ -use std::time::Duration; - -use anyhow::{anyhow, Context}; -use aws_sdk_ec2::Client; -use diff::Diff; -use futures_util::future::join_all; -use helpers::{all_logs_success, to_monitor_name}; -use mungos::mongodb::bson::{doc, to_bson}; -use types::{ - monitor_timestamp, traits::Permissioned, AwsBuilderBuildConfig, Build, DockerContainerState, - Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version, -}; - -use crate::{ - auth::RequestUser, - cloud::aws::{ - create_ec2_client, create_instance_with_ami, terminate_ec2_instance, Ec2Instance, - }, - helpers::empty_or_only_spaces, - 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( - &self, - build_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let build = self.db.get_build(build_id).await?; - let permissions = build.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(build) - } else { - Err(anyhow!( - "user does not have required permissions on this build" - )) - } - } - - pub async fn create_build(&self, name: &str, user: &RequestUser) -> anyhow::Result { - 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), - docker_organization: self - .config - .docker_organizations - .get(0) - .map(|d| d.to_string()), - aws_config: Some(AwsBuilderBuildConfig::default()), - permissions: [(user.id.clone(), PermissionLevel::Update)] - .into_iter() - .collect(), - last_built_at: "never".to_string(), - created_at: start_ts.clone(), - updated_at: start_ts.clone(), - ..Default::default() - }; - let build_id = self - .db - .builds - .create_one(build) - .await - .context("failed at adding build to db")?; - let build = self.db.get_build(&build_id).await?; - let update = Update { - target: UpdateTarget::Build(build_id), - operation: Operation::CreateBuild, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(build) - } - - pub async fn create_full_build( - &self, - mut build: Build, - user: &RequestUser, - ) -> anyhow::Result { - build.id = self.create_build(&build.name, user).await?.id; - let build = self.update_build(build, user).await?; - Ok(build) - } - - pub async fn copy_build( - &self, - target_id: &str, - new_name: String, - user: &RequestUser, - ) -> anyhow::Result { - let mut build = self - .get_build_check_permissions(target_id, user, PermissionLevel::Update) - .await?; - build.name = new_name; - build.version = Version::default(); - let build = self.create_full_build(build, user).await?; - Ok(build) - } - - pub async fn delete_build(&self, build_id: &str, user: &RequestUser) -> anyhow::Result { - if self.build_action_states.busy(build_id).await { - return Err(anyhow!("build busy")); - } - let build = self - .get_build_check_permissions(build_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - self.db.builds.delete_one(build_id).await?; - let update = Update { - target: UpdateTarget::Build(build_id.to_string()), - operation: Operation::DeleteBuild, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - logs: vec![Log::simple( - "delete build", - format!("deleted build {}", build.name), - )], - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(build) - } - - pub async fn update_build( - &self, - new_build: Build, - user: &RequestUser, - ) -> anyhow::Result { - if self.build_action_states.busy(&new_build.id).await { - return Err(anyhow!("build busy")); - } - let id = new_build.id.clone(); - self.build_action_states - .update_entry(id.clone(), |entry| { - entry.updating = true; - }) - .await; - - let res = self.update_build_inner(new_build, user).await; - - self.build_action_states - .update_entry(id.clone(), |entry| { - entry.updating = false; - }) - .await; - res - } - - async fn update_build_inner( - &self, - mut new_build: Build, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let current_build = self - .get_build_check_permissions(&new_build.id, user, PermissionLevel::Update) - .await?; - - 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.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(); - - // filter out any build args that contain empty strings - // these could only happen by accident - new_build.docker_build_args = new_build.docker_build_args.map(|mut args| { - args.build_args = args - .build_args - .into_iter() - .filter(|a| !empty_or_only_spaces(&a.variable) && !empty_or_only_spaces(&a.value)) - .collect(); - args - }); - - self.db - .builds - .update_one(&new_build.id, mungos::Update::Regular(new_build.clone())) - .await - .context("failed at update one build")?; - - let diff = current_build.diff(&new_build); - - let update = Update { - operation: Operation::UpdateBuild, - target: UpdateTarget::Build(new_build.id.clone()), - start_ts, - 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?; - - // 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(¤t_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; - - self.add_update(update).await?; - - Ok(new_build) - } - - pub async fn build(&self, build_id: &str, user: &RequestUser) -> anyhow::Result { - if self.build_action_states.busy(build_id).await { - return Err(anyhow!("build busy")); - } - self.build_action_states - .update_entry(build_id.to_string(), |entry| { - entry.building = true; - }) - .await; - let res = self.build_inner(build_id, user).await; - self.build_action_states - .update_entry(build_id.to_string(), |entry| { - entry.building = false; - }) - .await; - res - } - - async fn build_inner(&self, build_id: &str, user: &RequestUser) -> anyhow::Result { - let mut build = self - .get_build_check_permissions(build_id, user, PermissionLevel::Execute) - .await?; - build.version.increment(); - let mut update = Update { - target: UpdateTarget::Build(build_id.to_string()), - operation: Operation::BuildBuild, - start_ts: monitor_timestamp(), - status: UpdateStatus::InProgress, - operator: user.id.clone(), - success: true, - 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); - all_logs_success(&update.logs) - } - 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.server, &build) - .await - .context("failed at call to periphery to build") - { - Ok(logs) => logs, - Err(e) => Some(vec![Log::error("build", format!("{e:#?}"))]), - }; - - match build_logs { - Some(logs) => { - let success = all_logs_success(&logs); - update.logs.extend(logs); - if success { - let _ = self - .db - .builds - .update_one::( - build_id, - mungos::Update::Set(doc! { - "version": to_bson(&build.version) - .context("failed at converting version to bson")?, - "last_built_at": monitor_timestamp(), - }), - ) - .await; - } - } - None => { - update - .logs - .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; - } - - self.handle_post_build_redeploy(build_id, &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) - } - - async fn handle_post_build_redeploy(&self, build_id: &str, update: &mut Update) { - let redeploy_deployments = self - .db - .deployments - .get_some( - doc! { "build_id": build_id, "redeploy_on_build": true }, - None, - ) - .await; - - if let Ok(deployments) = redeploy_deployments { - let futures = deployments.into_iter().map(|d| async move { - let request_user = RequestUser { - id: "auto redeploy".to_string(), - is_admin: true, - ..Default::default() - }; - let state = self - .get_deployment_with_container_state(&request_user, &d.id) - .await - .map(|r| r.state) - .unwrap_or_default(); - if state == DockerContainerState::Running { - Some(( - d.id.clone(), - self.deploy_container( - &d.id, - &RequestUser { - id: "auto redeploy".to_string(), - is_admin: true, - ..Default::default() - }, - None, - None, - ) - .await, - )) - } else { - None - } - }); - - let redeploy_results = join_all(futures).await; - - let mut redeploys = Vec::::new(); - let mut redeploy_failures = Vec::::new(); - - for res in redeploy_results { - if res.is_none() { - continue; - } - let (id, res) = res.unwrap(); - match res { - Ok(_) => redeploys.push(id), - Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")), - } - } - - if redeploys.len() > 0 { - update.logs.push(Log::simple( - "redeploy", - format!("redeployed deployments: {}", redeploys.join(", ")), - )) - } - - if redeploy_failures.len() > 0 { - update.logs.push(Log::simple( - "redeploy failures", - redeploy_failures.join("\n"), - )) - } - } else if let Err(e) = redeploy_deployments { - update.logs.push(Log::simple( - "redeploys failed", - format!("failed to get deployments to redeploy: {e:#?}"), - )) - } - } - - async fn create_ec2_instance_for_build( - &self, - build: &Build, - ) -> anyhow::Result<(Ec2Instance, Option, Vec)> { - if build.aws_config.is_none() { - return Err(anyhow!("build has no aws_config attached")); - } - 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_name = aws_config - .ami_name - .as_ref() - .unwrap_or(&self.config.aws.default_ami_name); - let ami_id = &self - .config - .aws - .available_ami_accounts - .get(ami_name) - .ok_or(anyhow!("no ami id associated with ami name {ami_name}"))? - .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() - }; - let start_connect_ts = monitor_timestamp(); - let mut res = Ok(String::new()); - for _ in 0..BUILDER_POLL_MAX_TRIES { - let version = self.periphery.get_version(&instance.server).await; - if let Ok(version) = version { - let connect_log = Log { - stage: "build instance connected".to_string(), - success: true, - stdout: format!("established contact with periphery on builder\nperiphery version: v{version}"), - start_ts: start_connect_ts, - end_ts: monitor_timestamp(), - ..Default::default() - }; - return Ok((instance, Some(aws_client), vec![start_log, connect_log])); - } - res = version; - 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: 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 { - // 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 { - // 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) - // } -} diff --git a/core/src/actions/command.rs b/core/src/actions/command.rs deleted file mode 100644 index 995cf5e2c..000000000 --- a/core/src/actions/command.rs +++ /dev/null @@ -1,238 +0,0 @@ -use anyhow::{anyhow, Context}; -use diff::Diff; -use helpers::all_logs_success; -use types::{ - monitor_timestamp, traits::Permissioned, Log, Operation, PeripheryCommand, - PeripheryCommandBuilder, PermissionLevel, Update, UpdateStatus, UpdateTarget, -}; - -use crate::{auth::RequestUser, state::State}; - -impl State { - pub async fn get_command_check_permissions( - &self, - command_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let command = self.db.get_command(command_id).await?; - let permissions = command.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(command) - } else { - Err(anyhow!( - "user does not have required permissions on this command" - )) - } - } - - pub async fn create_command( - &self, - name: &str, - server_id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_server_check_permissions(&server_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - let command = PeripheryCommandBuilder::default() - .name(name.to_string()) - .server_id(server_id) - .build() - .context("failed to build command")?; - let command_id = self - .db - .commands - .create_one(command) - .await - .context("failed at adding command to db")?; - let command = self.db.get_command(&command_id).await?; - let update = Update { - target: UpdateTarget::Command(command_id), - operation: Operation::CreateCommand, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(command) - } - - pub async fn create_full_command( - &self, - mut command: PeripheryCommand, - user: &RequestUser, - ) -> anyhow::Result { - command.id = self - .create_command(&command.name, command.server_id.clone(), user) - .await? - .id; - let command = self.update_command(command, user).await?; - Ok(command) - } - - pub async fn copy_command( - &self, - target_id: &str, - new_name: String, - new_server_id: String, - user: &RequestUser, - ) -> anyhow::Result { - let mut command = self - .get_command_check_permissions(target_id, user, PermissionLevel::Update) - .await?; - command.name = new_name; - command.server_id = new_server_id; - let command = self.create_full_command(command, user).await?; - Ok(command) - } - - pub async fn delete_command( - &self, - command_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.command_action_states.busy(command_id).await { - return Err(anyhow!("command busy")); - } - let command = self - .get_command_check_permissions(command_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - self.db.commands.delete_one(command_id).await?; - let update = Update { - target: UpdateTarget::Command(command_id.to_string()), - operation: Operation::DeleteCommand, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - logs: vec![Log::simple( - "delete command", - format!("deleted command {}", command.name), - )], - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(command) - } - - pub async fn update_command( - &self, - mut new_command: PeripheryCommand, - user: &RequestUser, - ) -> anyhow::Result { - let current_command = self - .get_command_check_permissions(&new_command.id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - // none of these should be changed through this method - new_command.permissions = current_command.permissions.clone(); - new_command.server_id = current_command.server_id.clone(); - new_command.created_at = current_command.created_at.clone(); - new_command.updated_at = start_ts.clone(); - - self.db - .commands - .update_one( - &new_command.id, - mungos::Update::Regular(new_command.clone()), - ) - .await - .context("failed at update one command")?; - - let diff = current_command.diff(&new_command); - - let update = Update { - operation: Operation::UpdateCommand, - target: UpdateTarget::Command(new_command.id.clone()), - start_ts, - status: UpdateStatus::Complete, - logs: vec![Log::simple( - "command update", - serde_json::to_string_pretty(&diff).unwrap(), - )], - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - self.add_update(update.clone()).await?; - - self.update_update(update).await?; - - Ok(new_command) - } - - pub async fn run_command( - &self, - command_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.command_action_states.busy(command_id).await { - return Err(anyhow!("command busy")); - } - self.command_action_states - .update_entry(command_id.to_string(), |entry| { - entry.running = true; - }) - .await; - let res = self.run_command_inner(command_id, user).await; - self.command_action_states - .update_entry(command_id.to_string(), |entry| { - entry.running = false; - }) - .await; - res - } - - async fn run_command_inner( - &self, - command_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let command = self - .get_command_check_permissions(command_id, user, PermissionLevel::Execute) - .await?; - - if command.command.path.is_empty() || command.command.command.is_empty() { - return Err(anyhow!("command or path is empty, aborting")); - } - - let server = self.db.get_server(&command.server_id).await?; - - let mut update = Update { - target: UpdateTarget::Command(command_id.to_string()), - operation: Operation::RunCommand, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - match self.periphery.run_command(&server, &command.command).await { - Ok(log) => { - update.logs.push(log); - } - Err(e) => { - update - .logs - .push(Log::error("clone repo", format!("{e:#?}"))); - } - } - - 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) - } -} diff --git a/core/src/actions/deployment.rs b/core/src/actions/deployment.rs deleted file mode 100644 index ef0058663..000000000 --- a/core/src/actions/deployment.rs +++ /dev/null @@ -1,906 +0,0 @@ -use anyhow::{anyhow, Context}; -use diff::Diff; -use helpers::{all_logs_success, to_monitor_name}; -use mungos::mongodb::bson::doc; -use types::{ - monitor_timestamp, traits::Permissioned, Deployment, DeploymentBuilder, - DeploymentWithContainerState, DockerContainerState, Log, Operation, PermissionLevel, - ServerStatus, ServerWithStatus, TerminationSignal, Update, UpdateStatus, UpdateTarget, -}; - -use crate::{ - auth::RequestUser, - helpers::{any_option_diff_is_some, empty_or_only_spaces, get_image_name, option_diff_is_some}, - state::State, -}; - -impl State { - pub async fn get_deployment_check_permissions( - &self, - deployment_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let deployment = self.db.get_deployment(deployment_id).await?; - let permissions = deployment.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(deployment) - } else { - Err(anyhow!( - "user does not have required permissions on this deployment" - )) - } - } - - pub async fn create_deployment( - &self, - name: &str, - server_id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_server_check_permissions(&server_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - let mut deployment = DeploymentBuilder::default() - .name(to_monitor_name(name)) - .server_id(server_id) - .build() - .context("failed to build deployment")?; - deployment.permissions = [(user.id.clone(), PermissionLevel::Update)] - .into_iter() - .collect(); - deployment.created_at = start_ts.clone(); - deployment.updated_at = start_ts.clone(); - - let deployment_id = self - .db - .deployments - .create_one(deployment) - .await - .context("failed to add deployment to db")?; - let deployment = self.db.get_deployment(&deployment_id).await?; - let update = Update { - target: UpdateTarget::Deployment(deployment_id), - operation: Operation::CreateDeployment, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(deployment) - } - - pub async fn create_full_deployment( - &self, - mut deployment: Deployment, - user: &RequestUser, - ) -> anyhow::Result { - deployment.id = self - .create_deployment(&deployment.name, deployment.server_id.clone(), user) - .await? - .id; - let deployment = self.update_deployment(deployment, user).await?; - Ok(deployment) - } - - pub async fn copy_deployment( - &self, - target_id: &str, - new_name: String, - new_server_id: String, - user: &RequestUser, - ) -> anyhow::Result { - let mut deployment = self - .get_deployment_check_permissions(target_id, user, PermissionLevel::Update) - .await?; - deployment.name = new_name; - deployment.server_id = new_server_id; - let deployment = self.create_full_deployment(deployment, user).await?; - Ok(deployment) - } - - pub async fn delete_deployment( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - let server = self.db.get_server(&deployment.server_id).await?; - let log = match self - .periphery - .container_remove(&server, &deployment.name, stop_signal, stop_time) - .await - { - Ok(log) => log, - Err(e) => Log::error("destroy container", format!("{e:#?}")), - }; - self.db - .deployments - .delete_one(deployment_id) - .await - .context(format!( - "failed at deleting deployment at {deployment_id} from mongo" - ))?; - let update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::DeleteDeployment, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - logs: vec![ - log, - Log::simple( - "delete deployment", - format!( - "deleted deployment {} on server {}", - deployment.name, server.name - ), - ), - ], - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(deployment) - } - - pub async fn update_deployment( - &self, - new_deployment: Deployment, - user: &RequestUser, - ) -> anyhow::Result { - if self.deployment_action_states.busy(&new_deployment.id).await { - return Err(anyhow!("deployment busy")); - } - let id = new_deployment.id.clone(); - - self.deployment_action_states - .update_entry(id.clone(), |entry| { - entry.updating = true; - }) - .await; - - let res = self.update_deployment_inner(new_deployment, user).await; - - self.deployment_action_states - .update_entry(id.clone(), |entry| { - entry.updating = false; - }) - .await; - - res - } - - async fn update_deployment_inner( - &self, - mut new_deployment: Deployment, - user: &RequestUser, - ) -> anyhow::Result { - let current_deployment = self - .get_deployment_check_permissions(&new_deployment.id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - // none of these should be changed through this method - new_deployment.name = current_deployment.name.clone(); - new_deployment.permissions = current_deployment.permissions.clone(); - new_deployment.server_id = current_deployment.server_id.clone(); - new_deployment.created_at = current_deployment.created_at.clone(); - new_deployment.updated_at = start_ts.clone(); - - // filter out any volumes, ports, env vars, extra args which are or contain empty strings - // these could only happen by accident - new_deployment.docker_run_args.volumes = new_deployment - .docker_run_args - .volumes - .into_iter() - .filter(|v| !empty_or_only_spaces(&v.local) && !empty_or_only_spaces(&v.container)) - .collect(); - new_deployment.docker_run_args.ports = new_deployment - .docker_run_args - .ports - .into_iter() - .filter(|p| !empty_or_only_spaces(&p.local) && !empty_or_only_spaces(&p.container)) - .collect(); - new_deployment.docker_run_args.environment = new_deployment - .docker_run_args - .environment - .into_iter() - .filter(|e| !empty_or_only_spaces(&e.variable) && !empty_or_only_spaces(&e.value)) - .collect(); - new_deployment.docker_run_args.extra_args = new_deployment - .docker_run_args - .extra_args - .into_iter() - .filter(|a| a.len() != 0) - .collect(); - - self.db - .deployments - .update_one( - &new_deployment.id, - mungos::Update::Regular(new_deployment.clone()), - ) - .await - .context("failed at update one deployment")?; - - let diff = current_deployment.diff(&new_deployment); - - let mut update = Update { - operation: Operation::UpdateDeployment, - target: UpdateTarget::Deployment(new_deployment.id.clone()), - start_ts, - status: UpdateStatus::InProgress, - logs: vec![Log::simple( - "deployment update", - serde_json::to_string_pretty(&diff).unwrap(), - )], - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - 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(¤t_deployment.server_id).await?; - match self.periphery.clone_repo(&server, &new_deployment).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; - - self.update_update(update).await?; - - Ok(new_deployment) - } - - pub async fn rename_deployment( - &self, - deployment_id: &str, - new_name: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.renaming = true; - }) - .await; - - let res = self - .rename_deployment_inner(deployment_id, new_name, user) - .await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.renaming = false; - }) - .await; - res - } - - async fn rename_deployment_inner( - &self, - deployment_id: &str, - new_name: &str, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Update) - .await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::RenameDeployment, - start_ts, - status: UpdateStatus::InProgress, - operator: user.id.to_string(), - success: true, - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - let server_with_status = self.get_server(&deployment.server_id, user).await; - if server_with_status.is_err() { - update.logs.push(Log::error( - "get server", - format!( - "failed to get server info: {:?}", - server_with_status.as_ref().err().unwrap() - ), - )); - update.status = UpdateStatus::Complete; - update.end_ts = monitor_timestamp().into(); - update.success = false; - self.update_update(update).await?; - return Err(server_with_status.err().unwrap()); - } - let ServerWithStatus { server, status } = server_with_status.unwrap(); - if status != ServerStatus::Ok { - update.logs.push(Log::error( - "check server status", - String::from("cannot rename deployment when periphery is disabled or unreachable"), - )); - update.status = UpdateStatus::Complete; - update.end_ts = monitor_timestamp().into(); - update.success = false; - self.update_update(update).await?; - return Err(anyhow!( - "cannot rename deployment when periphery is disabled or unreachable" - )); - } - let deployment_state = self - .get_deployment_with_container_state(user, deployment_id) - .await; - if deployment_state.is_err() { - update.logs.push(Log::error( - "check deployment status", - format!( - "could not get current state of deployment: {:?}", - deployment_state.as_ref().err().unwrap() - ), - )); - update.status = UpdateStatus::Complete; - update.end_ts = monitor_timestamp().into(); - update.success = false; - self.update_update(update).await?; - return Err(deployment_state.err().unwrap()); - } - let DeploymentWithContainerState { - deployment, state, .. - } = deployment_state.unwrap(); - if state != DockerContainerState::NotDeployed { - let log = self - .periphery - .container_rename(&server, &deployment.name, new_name) - .await; - if log.is_err() { - update.logs.push(Log::error( - "rename container", - format!("{:?}", log.as_ref().err().unwrap()), - )); - update.status = UpdateStatus::Complete; - update.end_ts = monitor_timestamp().into(); - update.success = false; - self.update_update(update).await?; - return Err(log.err().unwrap()); - } - let log = log.unwrap(); - if !log.success { - update.logs.push(log); - update.status = UpdateStatus::Complete; - update.end_ts = monitor_timestamp().into(); - update.success = false; - self.update_update(update).await?; - return Err(anyhow!("rename container on periphery not successful")); - } - update.logs.push(log); - } - let res = self - .db - .deployments - .update_one( - deployment_id, - mungos::Update::<()>::Set( - doc! { "name": to_monitor_name(new_name), "updated_at": monitor_timestamp() }, - ), - ) - .await - .context("failed to update deployment name on mongo"); - if let Err(e) = res { - update - .logs - .push(Log::error("mongo update", format!("{e:?}"))); - } else { - update.logs.push(Log::simple( - "mongo update", - String::from("updated name on mongo"), - )) - } - - if deployment.repo.is_some() { - let res = self.reclone_deployment(deployment_id, user, false).await; - if let Err(e) = res { - update - .logs - .push(Log::error("reclone repo", format!("{e:?}"))); - } else { - update.logs.push(Log::simple( - "reclone repo", - "deployment repo cloned with new name".to_string(), - )); - } - } - - update.end_ts = monitor_timestamp().into(); - update.status = UpdateStatus::Complete; - update.success = all_logs_success(&update.logs); - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn reclone_deployment( - &self, - deployment_id: &str, - user: &RequestUser, - check_deployment_busy: bool, - ) -> anyhow::Result { - if check_deployment_busy && self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.recloning = true; - }) - .await; - - let res = self.reclone_deployment_inner(deployment_id, user).await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.recloning = false; - }) - .await; - - res - } - - async fn reclone_deployment_inner( - &self, - deployment_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::RecloneDeployment, - start_ts: monitor_timestamp(), - status: UpdateStatus::InProgress, - operator: user.id.clone(), - success: true, - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - match self.periphery.clone_repo(&server, &deployment).await { - Ok(clone_logs) => { - update.logs.extend(clone_logs); - } - Err(e) => { - update - .logs - .push(Log::error("clone repo", format!("{e:#?}"))); - } - }; - - 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 deploy_container( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.deploying = true; - }) - .await; - - let res = self - .deploy_container_inner(deployment_id, user, stop_signal, stop_time) - .await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.deploying = false; - }) - .await; - res - } - - async fn deploy_container_inner( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let mut deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let version = if let Some(build_id) = &deployment.build_id { - let build = self.db.get_build(build_id).await?; - let image = get_image_name(&build); - if deployment.docker_run_args.docker_account.is_none() { - if let Some(docker_account) = &build.docker_account { - deployment.docker_run_args.docker_account = Some(docker_account.to_string()) - }; - } - let version = if let Some(version) = &deployment.build_version { - version.clone() - } else { - build.version.clone() - }; - deployment.docker_run_args.image = format!("{image}:{}", version.to_string()); - Some(version) - } else { - None - }; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::DeployContainer, - start_ts, - status: UpdateStatus::InProgress, - operator: user.id.clone(), - success: true, - version, - ..Default::default() - }; - - update.id = self.add_update(update.clone()).await?; - - let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into(); - let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into(); - - let deploy_log = match self - .periphery - .deploy(&server, &deployment, stop_signal, stop_time) - .await - { - Ok(log) => log, - Err(e) => Log::error("deploy container", format!("{e:#?}")), - }; - - update.success = deploy_log.success; - update.logs.push(deploy_log); - update.status = UpdateStatus::Complete; - update.end_ts = Some(monitor_timestamp()); - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn start_container( - &self, - deployment_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.starting = true; - }) - .await; - - let res = self.start_container_inner(deployment_id, user).await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.starting = false; - }) - .await; - res - } - - async fn start_container_inner( - &self, - deployment_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::StartContainer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let log = self - .periphery - .container_start(&server, &deployment.name) - .await; - - update.success = match log { - Ok(log) => { - let success = log.success; - update.logs.push(log); - success - } - Err(e) => { - update - .logs - .push(Log::error("start container", format!("{e:#?}"))); - false - } - }; - - update.end_ts = Some(monitor_timestamp()); - update.status = UpdateStatus::Complete; - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn stop_container( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.stopping = true; - }) - .await; - - let res = self - .stop_container_inner(deployment_id, user, stop_signal, stop_time) - .await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.stopping = false; - }) - .await; - res - } - - async fn stop_container_inner( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::StopContainer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into(); - let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into(); - - let log = self - .periphery - .container_stop(&server, &deployment.name, stop_signal, stop_time) - .await; - - update.success = match log { - Ok(log) => { - let success = log.success; - update.logs.push(log); - success - } - Err(e) => { - update - .logs - .push(Log::error("stop container", format!("{e:#?}"))); - false - } - }; - - update.end_ts = Some(monitor_timestamp()); - update.status = UpdateStatus::Complete; - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn remove_container( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.removing = true; - }) - .await; - - let res = self - .remove_container_inner(deployment_id, user, stop_signal, stop_time) - .await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.removing = false; - }) - .await; - res - } - - async fn remove_container_inner( - &self, - deployment_id: &str, - user: &RequestUser, - stop_signal: Option, - stop_time: Option, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::RemoveContainer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into(); - let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into(); - - let log = self - .periphery - .container_remove(&server, &deployment.name, stop_signal, stop_time) - .await; - - update.success = match log { - Ok(log) => { - let success = log.success; - update.logs.push(log); - success - } - Err(e) => { - update - .logs - .push(Log::error("remove container", format!("{e:#?}"))); - false - } - }; - - update.end_ts = Some(monitor_timestamp()); - update.status = UpdateStatus::Complete; - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn pull_deployment_repo( - &self, - deployment_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.deployment_action_states.busy(deployment_id).await { - return Err(anyhow!("deployment busy")); - } - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.pulling = true; - }) - .await; - - let res = self.pull_deployment_repo_inner(deployment_id, user).await; - - self.deployment_action_states - .update_entry(deployment_id.to_string(), |entry| { - entry.pulling = false; - }) - .await; - res - } - - async fn pull_deployment_repo_inner( - &self, - deployment_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let deployment = self - .get_deployment_check_permissions(deployment_id, user, PermissionLevel::Execute) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let mut update = Update { - target: UpdateTarget::Deployment(deployment_id.to_string()), - operation: Operation::PullDeployment, - start_ts, - status: UpdateStatus::InProgress, - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - update.id = self.add_update(update.clone()).await?; - - let logs = self - .periphery - .pull_repo( - &server, - &deployment.name, - &deployment.branch, - &deployment.on_pull, - ) - .await?; - - update.success = all_logs_success(&logs); - update.logs.extend(logs); - update.end_ts = Some(monitor_timestamp()); - update.status = UpdateStatus::Complete; - - self.update_update(update.clone()).await?; - - Ok(update) - } -} diff --git a/core/src/actions/group.rs b/core/src/actions/group.rs deleted file mode 100644 index 605fcf162..000000000 --- a/core/src/actions/group.rs +++ /dev/null @@ -1,141 +0,0 @@ -use anyhow::{anyhow, Context}; -use diff::Diff; -use helpers::to_monitor_name; -use types::{ - monitor_timestamp, traits::Permissioned, Group, Log, Operation, PermissionLevel, Update, - UpdateStatus, UpdateTarget, -}; - -use crate::{auth::RequestUser, state::State}; - -impl State { - pub async fn get_group_check_permissions( - &self, - group_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let group = self.db.get_group(group_id).await?; - let permissions = group.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(group) - } else { - Err(anyhow!( - "user does not have required permissions on this group" - )) - } - } - - pub async fn create_group(&self, name: &str, user: &RequestUser) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let group = Group { - name: to_monitor_name(name), - permissions: [(user.id.clone(), PermissionLevel::Update)] - .into_iter() - .collect(), - created_at: start_ts.clone(), - updated_at: start_ts.clone(), - ..Default::default() - }; - let group_id = self - .db - .groups - .create_one(group) - .await - .context("failed to add group to db")?; - let group = self.db.get_group(&group_id).await?; - let update = Update { - target: UpdateTarget::Group(group_id), - operation: Operation::CreateGroup, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(group) - } - - pub async fn create_full_group( - &self, - mut full_group: Group, - user: &RequestUser, - ) -> anyhow::Result { - let group = self.create_group(&full_group.name, user).await?; - full_group.id = group.id; - let group = self.update_group(full_group, user).await?; - Ok(group) - } - - pub async fn delete_group(&self, id: &str, user: &RequestUser) -> anyhow::Result { - let group = self - .get_group_check_permissions(id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - self.db - .groups - .delete_one(id) - .await - .context(format!("failed at deleting group at {id} from mongo"))?; - let update = Update { - target: UpdateTarget::Group(id.to_string()), - operation: Operation::DeleteGroup, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - logs: vec![Log::simple( - "delete group", - format!("deleted group {}", group.name), - )], - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(group) - } - - pub async fn update_group( - &self, - mut new_group: Group, - user: &RequestUser, - ) -> anyhow::Result { - let current_group = self - .get_group_check_permissions(&new_group.id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - // none of these should be changed through this method - new_group.name = current_group.name.clone(); - new_group.permissions = current_group.permissions.clone(); - new_group.created_at = current_group.created_at.clone(); - new_group.updated_at = start_ts.clone(); - - self.db - .groups - .update_one(&new_group.id, mungos::Update::Regular(new_group.clone())) - .await - .context("failed at update one group")?; - - let diff = current_group.diff(&new_group); - - let update = Update { - operation: Operation::UpdateGroup, - target: UpdateTarget::Group(new_group.id.clone()), - end_ts: Some(start_ts.clone()), - start_ts, - status: UpdateStatus::Complete, - logs: vec![Log::simple( - "group update", - serde_json::to_string_pretty(&diff).unwrap(), - )], - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - self.add_update(update).await?; - - Ok(new_group) - } -} diff --git a/core/src/actions/mod.rs b/core/src/actions/mod.rs deleted file mode 100644 index 718d761f6..000000000 --- a/core/src/actions/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::Context; -use types::Update; - -use crate::state::State; - -mod build; -mod command; -mod deployment; -mod group; -mod procedure; -mod server; - -impl State { - pub async fn send_update(&self, update: Update) -> anyhow::Result<()> { - self.update.sender.lock().await.send(update)?; - Ok(()) - } - - pub async fn add_update(&self, mut update: Update) -> anyhow::Result { - update.id = self - .db - .updates - .create_one(update.clone()) - .await - .context("failed to insert update into db")? - .to_string(); - let id = update.id.clone(); - let _ = self.send_update(update).await; - Ok(id) - } - - pub async fn update_update(&self, mut update: Update) -> anyhow::Result<()> { - let mut update_id = String::new(); - std::mem::swap(&mut update.id, &mut update_id); - self.db - .updates - .update_one(&update_id, mungos::Update::Regular(update.clone())) - .await - .context("failed to update the update on db. the update build process was deleted")?; - std::mem::swap(&mut update.id, &mut update_id); - let _ = self.send_update(update).await; - Ok(()) - } -} diff --git a/core/src/actions/procedure.rs b/core/src/actions/procedure.rs deleted file mode 100644 index 15167dbf1..000000000 --- a/core/src/actions/procedure.rs +++ /dev/null @@ -1,299 +0,0 @@ -use anyhow::{anyhow, Context}; -use diff::Diff; -use helpers::to_monitor_name; -use types::{ - monitor_timestamp, traits::Permissioned, Log, Operation, PermissionLevel, Procedure, - ProcedureOperation::*, ProcedureStage, Update, UpdateStatus, UpdateTarget, -}; - -use crate::{auth::RequestUser, state::State}; - -impl State { - pub async fn get_procedure_check_permissions( - &self, - procedure_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let procedure = self.db.get_procedure(procedure_id).await?; - let permissions = procedure.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(procedure) - } else { - Err(anyhow!( - "user does not have required permissions on this procedure" - )) - } - } - - pub async fn create_procedure( - &self, - name: &str, - user: &RequestUser, - ) -> anyhow::Result { - let start_ts = monitor_timestamp(); - let procedure = Procedure { - name: to_monitor_name(name), - permissions: [(user.id.clone(), PermissionLevel::Update)] - .into_iter() - .collect(), - created_at: start_ts.clone(), - updated_at: start_ts.clone(), - ..Default::default() - }; - let procedure_id = self - .db - .procedures - .create_one(procedure) - .await - .context("failed to add procedure to db")?; - let procedure = self.db.get_procedure(&procedure_id).await?; - let update = Update { - target: UpdateTarget::Procedure(procedure_id), - operation: Operation::CreateProcedure, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(procedure) - } - - pub async fn create_full_procedure( - &self, - mut full_procedure: Procedure, - user: &RequestUser, - ) -> anyhow::Result { - let procedure = self.create_procedure(&full_procedure.name, user).await?; - full_procedure.id = procedure.id; - let procedure = self.update_procedure(full_procedure, user).await?; - Ok(procedure) - } - - pub async fn delete_procedure( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let procedure = self - .get_procedure_check_permissions(id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - self.db - .procedures - .delete_one(id) - .await - .context(format!("failed at deleting procedure at {id} from mongo"))?; - let update = Update { - target: UpdateTarget::Procedure(id.to_string()), - operation: Operation::DeleteProcedure, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - logs: vec![Log::simple( - "delete procedure", - format!("deleted procedure {}", procedure.name), - )], - success: true, - ..Default::default() - }; - self.add_update(update).await?; - Ok(procedure) - } - - pub async fn update_procedure( - &self, - mut new_procedure: Procedure, - user: &RequestUser, - ) -> anyhow::Result { - let current_procedure = self - .get_procedure_check_permissions(&new_procedure.id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - // none of these should be changed through this method - new_procedure.name = current_procedure.name.clone(); - new_procedure.permissions = current_procedure.permissions.clone(); - new_procedure.created_at = current_procedure.created_at.clone(); - new_procedure.updated_at = start_ts.clone(); - - for ProcedureStage { - operation, - target_id, - } in &new_procedure.stages - { - match operation { - BuildBuild => { - self.get_build_check_permissions(&target_id, user, PermissionLevel::Execute) - .await?; - } - DeployContainer | StartContainer | StopContainer | RemoveContainer - | PullDeployment | RecloneDeployment => { - self.get_deployment_check_permissions( - target_id, - user, - PermissionLevel::Execute, - ) - .await?; - } - PruneImagesServer | PruneContainersServer | PruneNetworksServer => { - self.get_server_check_permissions(target_id, user, PermissionLevel::Execute) - .await?; - } - RunProcedure => { - self.get_procedure_check_permissions(target_id, user, PermissionLevel::Execute) - .await?; - } - None => {} - } - } - - self.db - .procedures - .update_one( - &new_procedure.id, - mungos::Update::Regular(new_procedure.clone()), - ) - .await - .context("failed at update one procedure")?; - - let diff = current_procedure.diff(&new_procedure); - - let update = Update { - operation: Operation::UpdateProcedure, - target: UpdateTarget::Procedure(new_procedure.id.clone()), - end_ts: Some(start_ts.clone()), - start_ts, - status: UpdateStatus::Complete, - logs: vec![Log::simple( - "procedure update", - serde_json::to_string_pretty(&diff).unwrap(), - )], - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - self.add_update(update).await?; - - Ok(new_procedure) - } - - pub async fn run_procedure( - &self, - procedure_id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let procedure = self - .get_procedure_check_permissions(procedure_id, user, PermissionLevel::Execute) - .await?; - let mut updates = Vec::new(); - for ProcedureStage { - operation, - target_id, - } in procedure.stages - { - match operation { - None => {} - // deployment - StartContainer => { - let update = self - .start_container(&target_id, user) - .await - .context(format!( - "failed at start container for deployment (id: {target_id})" - ))?; - updates.push(update); - } - StopContainer => { - let update = self - .stop_container(&target_id, user, Option::None, Option::None) - .await - .context(format!( - "failed at stop container for deployment (id: {target_id})" - ))?; - updates.push(update); - } - RemoveContainer => { - let update = self - .remove_container(&target_id, user, Option::None, Option::None) - .await - .context(format!( - "failed at remove container for deployment (id: {target_id})" - ))?; - updates.push(update); - } - DeployContainer => { - let update = self - .deploy_container(&target_id, user, Option::None, Option::None) - .await - .context(format!( - "failed at deploy container for deployment (id: {target_id})" - ))?; - updates.push(update); - } - RecloneDeployment => { - let update = self - .reclone_deployment(&target_id, user, true) - .await - .context(format!("failed at reclone deployment (id: {target_id})"))?; - updates.push(update); - } - PullDeployment => { - // implement this one - let update = self - .pull_deployment_repo(&target_id, user) - .await - .context(format!("failed at pull deployment (id: {target_id})"))?; - updates.push(update); - } - // build - BuildBuild => { - let update = self - .build(&target_id, user) - .await - .context(format!("failed at build (id: {target_id})"))?; - updates.push(update); - } - // server - PruneImagesServer => { - let update = self.prune_images(&target_id, user).await.context(format!( - "failed at prune images on server (id: {target_id})" - ))?; - updates.push(update); - } - PruneContainersServer => { - let update = self - .prune_containers(&target_id, user) - .await - .context(format!( - "failed at prune containers on server (id: {target_id})" - ))?; - updates.push(update); - } - PruneNetworksServer => { - let update = self - .prune_networks(&target_id, user) - .await - .context(format!( - "failed at prune networks on servers (id: {target_id})" - ))?; - updates.push(update); - } - // procedure - RunProcedure => { - // need to figure out async recursion - // need to guard against infinite procedure loops when they are updated - // let proc_updates = self - // .run_procedure(&target_id, user) - // .await - // .context(format!("failed to run nested procedure (id: {target_id})"))?; - // updates.extend(proc_updates); - } - } - } - Ok(updates) - } -} diff --git a/core/src/actions/server.rs b/core/src/actions/server.rs deleted file mode 100644 index 36ee96381..000000000 --- a/core/src/actions/server.rs +++ /dev/null @@ -1,391 +0,0 @@ -use anyhow::{anyhow, Context}; -use diff::Diff; -use futures_util::future::join_all; -use mungos::mongodb::bson::doc; -use types::{ - monitor_timestamp, traits::Permissioned, Log, Operation, PermissionLevel, Server, Update, - UpdateStatus, UpdateTarget, -}; - -use crate::{auth::RequestUser, state::State}; - -impl State { - pub async fn get_server_check_permissions( - &self, - server_id: &str, - user: &RequestUser, - permission_level: PermissionLevel, - ) -> anyhow::Result { - let server = self.db.get_server(server_id).await?; - let permissions = server.get_user_permissions(&user.id); - if user.is_admin || permissions >= permission_level { - Ok(server) - } else { - Err(anyhow!( - "user does not have required permissions on this server" - )) - } - } - - pub async fn create_server( - &self, - name: &str, - address: String, - user: &RequestUser, - ) -> anyhow::Result { - if !user.is_admin && !user.create_server_permissions { - return Err(anyhow!( - "user does not have permissions to add server (not admin)" - )); - } - let start_ts = monitor_timestamp(); - let server = Server { - name: name.to_string(), - address, - permissions: [(user.id.clone(), PermissionLevel::Update)] - .into_iter() - .collect(), - created_at: start_ts.clone(), - updated_at: start_ts.clone(), - ..Default::default() - }; - let server_id = self - .db - .servers - .create_one(server) - .await - .context("failed to add server to db")?; - let server = self.db.get_server(&server_id).await?; - let update = Update { - target: UpdateTarget::Server(server_id), - operation: Operation::CreateServer, - start_ts, - end_ts: Some(monitor_timestamp()), - operator: user.id.clone(), - success: true, - ..Default::default() - }; - self.add_update(update).await?; - - Ok(server) - } - - pub async fn create_full_server( - &self, - mut server: Server, - user: &RequestUser, - ) -> anyhow::Result { - server.id = self - .create_server(&server.name, server.address.clone(), user) - .await? - .id; - let server = self.update_server(server, user).await?; - Ok(server) - } - - pub async fn delete_server( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.server_action_states.busy(server_id).await { - return Err(anyhow!("server busy")); - } - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::Server(server_id.to_string()), - operation: Operation::DeleteServer, - start_ts, - operator: user.id.clone(), - success: true, - status: UpdateStatus::InProgress, - ..Default::default() - }; - 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, None, None).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, None, None).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::>>()?; - build_res.into_iter().collect::>>()?; - 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) - } - - pub async fn update_server( - &self, - mut new_server: Server, - user: &RequestUser, - ) -> anyhow::Result { - if self.server_action_states.busy(&new_server.id).await { - return Err(anyhow!("server busy")); - } - let current_server = self - .get_server_check_permissions(&new_server.id, user, PermissionLevel::Update) - .await?; - let start_ts = monitor_timestamp(); - - new_server.permissions = current_server.permissions.clone(); - new_server.created_at = current_server.created_at.clone(); - new_server.updated_at = start_ts.clone(); - - let diff = current_server.diff(&new_server); - - self.db - .servers - .update_one(&new_server.id, mungos::Update::Regular(new_server.clone())) - .await - .context("failed at update one server")?; - - let update = Update { - operation: Operation::UpdateServer, - target: UpdateTarget::Server(new_server.id.clone()), - start_ts, - end_ts: Some(monitor_timestamp()), - status: UpdateStatus::Complete, - logs: vec![Log::simple( - "server update", - serde_json::to_string_pretty(&diff).unwrap(), - )], - operator: user.id.clone(), - success: true, - ..Default::default() - }; - - self.add_update(update).await?; - Ok(new_server) - } - - pub async fn prune_networks( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.server_action_states.busy(server_id).await { - return Err(anyhow!("server busy")); - } - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_networks = true; - }) - .await; - - let res = self.prune_networks_inner(server_id, user).await; - - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_networks = false; - }) - .await; - res - } - - async fn prune_networks_inner( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Execute) - .await?; - - let start_ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::Server(server_id.to_owned()), - operation: Operation::PruneNetworksServer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let log = match self.periphery.network_prune(&server).await.context(format!( - "failed to prune networks on server {}", - server.name - )) { - Ok(log) => log, - Err(e) => Log::error("prune networks", format!("{e:#?}")), - }; - - update.success = log.success; - update.status = UpdateStatus::Complete; - update.end_ts = Some(monitor_timestamp()); - update.logs.push(log); - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn prune_images( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.server_action_states.busy(server_id).await { - return Err(anyhow!("server busy")); - } - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_images = true; - }) - .await; - - let res = self.prune_images_inner(server_id, user).await; - - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_images = false; - }) - .await; - res - } - - async fn prune_images_inner( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Execute) - .await?; - let start_ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::Server(server_id.to_owned()), - operation: Operation::PruneImagesServer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let log = match self - .periphery - .image_prune(&server) - .await - .context(format!("failed to prune images on server {}", server.name)) - { - Ok(log) => log, - Err(e) => Log::error("prune images", format!("{e:#?}")), - }; - - update.success = log.success; - update.status = UpdateStatus::Complete; - update.end_ts = Some(monitor_timestamp()); - update.logs.push(log); - - self.update_update(update.clone()).await?; - - Ok(update) - } - - pub async fn prune_containers( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - if self.server_action_states.busy(server_id).await { - return Err(anyhow!("server busy")); - } - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_containers = true; - }) - .await; - - let res = self.prune_containers_inner(server_id, user).await; - - self.server_action_states - .update_entry(server_id.to_string(), |entry| { - entry.pruning_containers = false; - }) - .await; - res - } - - async fn prune_containers_inner( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Execute) - .await?; - - let start_ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::Server(server_id.to_owned()), - operation: Operation::PruneContainersServer, - start_ts, - status: UpdateStatus::InProgress, - success: true, - operator: user.id.clone(), - ..Default::default() - }; - update.id = self.add_update(update.clone()).await?; - - let log = match self - .periphery - .container_prune(&server) - .await - .context(format!( - "failed to prune containers on server {}", - server.name - )) { - Ok(log) => log, - Err(e) => Log::error("prune containers", format!("{e:#?}")), - }; - - update.success = log.success; - update.status = UpdateStatus::Complete; - update.end_ts = Some(monitor_timestamp()); - update.logs.push(log); - - self.update_update(update.clone()).await?; - - Ok(update) - } -} diff --git a/core/src/api/build.rs b/core/src/api/build.rs deleted file mode 100644 index 6ffd26fa8..000000000 --- a/core/src/api/build.rs +++ /dev/null @@ -1,435 +0,0 @@ -use std::{cmp::Ordering, collections::HashMap}; - -use anyhow::Context; -use async_timing_util::unix_timestamp_ms; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, patch, post}, - Extension, Json, Router, -}; -use futures_util::TryStreamExt; -use helpers::handle_anyhow_error; -use mungos::mongodb::{ - bson::{doc, Document}, - options::FindOptions, -}; -use serde::{Deserialize, Serialize}; -use types::{ - monitor_ts_from_unix, traits::Permissioned, unix_from_monitor_ts, AwsBuilderConfig, Build, - BuildActionState, BuildVersionsReponse, Operation, PermissionLevel, UpdateStatus, -}; -use typeshare::typeshare; - -const NUM_VERSIONS_PER_PAGE: u64 = 10; -const ONE_DAY_MS: i64 = 86400000; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -use super::spawn_request_action; - -#[derive(Serialize, Deserialize)] -struct BuildId { - id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct CreateBuildBody { - name: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct CopyBuildBody { - name: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct BuildVersionsQuery { - #[serde(default)] - page: u32, - major: Option, - minor: Option, - patch: Option, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct BuildStatsQuery { - #[serde(default)] - page: u32, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct BuildStatsResponse { - pub total_time: f64, // in hours - pub total_count: f64, // number of builds - pub days: Vec, -} - -#[typeshare] -#[derive(Serialize, Deserialize, Default)] -pub struct BuildStatsDay { - pub time: f64, - pub count: f64, - pub ts: f64, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(build_id): Path| async move { - let build = state - .get_build_check_permissions(&build_id.id, &user, PermissionLevel::Read) - .await - .map_err(handle_anyhow_error)?; - response!(Json(build)) - }, - ), - ) - .route( - "/list", - get( - |state: StateExtension, - Extension(user): RequestUserExtension, - Query(query): Query| async move { - let builds = state - .list_builds(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(builds)) - }, - ), - ) - .route( - "/create", - post( - |state: StateExtension, - Extension(user): RequestUserExtension, - Json(build): Json| async move { - let build = state - .create_build(&build.name, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(build)) - }, - ), - ) - .route( - "/create_full", - post( - |state: StateExtension, - Extension(user): RequestUserExtension, - Json(build): Json| async move { - let build = spawn_request_action(async move { - state - .create_full_build(build, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(build)) - }, - ), - ) - .route( - "/:id/copy", - post( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(BuildId { id }): Path, - Json(build): Json| async move { - let build = spawn_request_action(async move { - state - .copy_build(&id, build.name, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(build)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(build_id): Path| async move { - let build = spawn_request_action(async move { - state - .delete_build(&build_id.id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(build)) - }, - ), - ) - .route( - "/update", - patch( - |state: StateExtension, - Extension(user): RequestUserExtension, - Json(build): Json| async move { - let build = spawn_request_action(async move { - state - .update_build(build, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(build)) - }, - ), - ) - .route( - "/:id/build", - post( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(build_id): Path| async move { - let update = spawn_request_action(async move { - state - .build(&build_id.id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/action_state", - get( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(BuildId { id }): Path| async move { - let action_state = state - .get_build_action_states(id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(action_state)) - }, - ), - ) - .route( - "/:id/versions", - get( - |state: StateExtension, - Extension(user): RequestUserExtension, - Path(BuildId { id }), - Query(query): Query| async move { - let versions = state - .get_build_versions(&id, &user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(versions)) - }, - ), - ) - .route( - "/aws_builder_defaults", - get(|state: StateExtension| async move { - Json(AwsBuilderConfig { - access_key_id: String::new(), - secret_access_key: String::new(), - ..state.config.aws.clone() - }) - }), - ) - .route( - "/docker_organizations", - get(|state: StateExtension| async move { - Json(state.config.docker_organizations.clone()) - }), - ) - .route( - "/stats", - get(|state: StateExtension, query: Query| async move { - let stats = state.get_build_stats(query.page).await.map_err(handle_anyhow_error)?; - response!(Json(stats)) - }), - ) -} - -impl State { - async fn list_builds( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let builds: Vec = self - .db - .builds - .get_some(query, None) - .await - .context("failed at get all builds query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .collect(); - Ok(builds) - } - - async fn get_build_action_states( - &self, - id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_build_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let action_state = self.build_action_states.get_or_default(id).await; - Ok(action_state) - } - - async fn get_build_versions( - &self, - id: &str, - user: &RequestUser, - query: BuildVersionsQuery, - ) -> anyhow::Result> { - self.get_build_check_permissions(&id, user, PermissionLevel::Read) - .await?; - let mut filter = doc! { - "target": { - "type": "Build", - "id": id - }, - "operation": Operation::BuildBuild.to_string(), - "status": UpdateStatus::Complete.to_string(), - "success": true - }; - if let Some(major) = query.major { - filter.insert("version.major", major); - } - if let Some(minor) = query.minor { - filter.insert("version.minor", minor); - } - if let Some(patch) = query.patch { - filter.insert("version.patch", patch); - } - let versions = self - .db - .updates - .get_some( - filter, - FindOptions::builder() - .sort(doc! { "_id": -1 }) - .limit(NUM_VERSIONS_PER_PAGE as i64) - .skip(query.page as u64 * NUM_VERSIONS_PER_PAGE) - .build(), - ) - .await - .context("failed to pull versions from mongo")? - .into_iter() - .map(|u| (u.version, u.start_ts)) - .filter(|(v, _)| v.is_some()) - .map(|(v, ts)| BuildVersionsReponse { - version: v.unwrap(), - ts, - }) - .collect(); - Ok(versions) - } - - async fn get_build_stats(&self, page: u32) -> anyhow::Result { - let curr_ts = unix_timestamp_ms() as i64; - let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; - - let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS; - let open_ts = close_ts - 30 * ONE_DAY_MS; - - let mut build_updates = self - .db - .updates - .collection - .find( - doc! { - "start_ts": { - "$gte": monitor_ts_from_unix(open_ts) - .context("open_ts out of bounds")?, - "$lt": monitor_ts_from_unix(close_ts) - .context("close_ts out of bounds")? - }, - "operation": Operation::BuildBuild.to_string(), - }, - None, - ) - .await?; - - let mut days = HashMap::::with_capacity(32); - - let mut curr = open_ts; - - while curr < close_ts { - let stats = BuildStatsDay { - ts: curr as f64, - ..Default::default() - }; - days.insert(curr, stats); - curr += ONE_DAY_MS; - } - - while let Some(update) = build_updates.try_next().await? { - if let Some(end_ts) = update.end_ts { - let start_ts = unix_from_monitor_ts(&update.start_ts) - .context("failed to parse update start_ts")?; - let end_ts = - unix_from_monitor_ts(&end_ts).context("failed to parse update end_ts")?; - let day = start_ts - start_ts % ONE_DAY_MS; - let mut entry = days.entry(day).or_default(); - entry.count += 1.0; - entry.time += ms_to_hour(end_ts - start_ts); - } - } - - Ok(BuildStatsResponse::new(days.into_values().collect())) - } -} - -impl BuildStatsResponse { - fn new(mut days: Vec) -> BuildStatsResponse { - days.sort_by(|a, b| { - if a.ts < b.ts { - Ordering::Less - } else { - Ordering::Greater - } - }); - let mut total_time = 0.0; - let mut total_count = 0.0; - for day in &days { - total_time += day.time; - total_count += day.count; - } - BuildStatsResponse { - total_time, - total_count, - days, - } - } -} - -const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0; -fn ms_to_hour(duration: i64) -> f64 { - duration as f64 / MS_TO_HOUR_DIVISOR -} diff --git a/core/src/api/command.rs b/core/src/api/command.rs deleted file mode 100644 index 2315511e8..000000000 --- a/core/src/api/command.rs +++ /dev/null @@ -1,220 +0,0 @@ -use anyhow::Context; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, patch, post}, - Json, Router, -}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::Document; -use serde::{Deserialize, Serialize}; -use types::{traits::Permissioned, CommandActionState, PeripheryCommand, PermissionLevel}; -use typeshare::typeshare; - -use crate::{ - api::spawn_request_action, - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -#[derive(Serialize, Deserialize)] -pub struct CommandId { - id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CreateCommandBody { - name: String, - server_id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CopyCommandBody { - name: String, - server_id: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(CommandId { id })| async move { - let command = state - .get_command_check_permissions(&id, &user, PermissionLevel::Read) - .await - .map_err(handle_anyhow_error)?; - response!(Json(command)) - }, - ), - ) - .route( - "/list", - get( - |state: StateExtension, - user: RequestUserExtension, - Query(query): Query| async move { - let commands = state - .list_commands(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(commands)) - }, - ), - ) - .route( - "/create", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(command): Json| async move { - let command = state - .create_command(&command.name, command.server_id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(command)) - }, - ), - ) - .route( - "/create_full", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(command): Json| async move { - let command = spawn_request_action(async move { - state - .create_full_command(command, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(command)) - }, - ), - ) - .route( - "/:id/copy", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(CommandId { id }), - Json(command): Json| async move { - let command = spawn_request_action(async move { - state - .copy_command(&id, command.name, command.server_id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(command)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |state: StateExtension, - user: RequestUserExtension, - Path(CommandId { id })| async move { - let build = spawn_request_action(async move { - state - .delete_command(&id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(build)) - }, - ), - ) - .route( - "/update", - patch( - |state: StateExtension, - user: RequestUserExtension, - Json(command): Json| async move { - let command = spawn_request_action(async move { - state - .update_command(command, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(command)) - }, - ), - ) - .route( - "/:id/action_state", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(CommandId { id })| async move { - let action_state = state - .get_command_action_states(id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(action_state)) - }, - ), - ) - .route( - "/:id/run", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(CommandId { id })| async move { - let update = spawn_request_action(async move { - state - .run_command(&id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) -} - -impl State { - async fn list_commands( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let commands: Vec = self - .db - .commands - .get_some(query, None) - .await - .context("failed at get all commands query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .collect(); - Ok(commands) - } - - async fn get_command_action_states( - &self, - id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_command_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let action_state = self.command_action_states.get_or_default(id).await; - Ok(action_state) - } -} diff --git a/core/src/api/deployment.rs b/core/src/api/deployment.rs deleted file mode 100644 index c60d700d8..000000000 --- a/core/src/api/deployment.rs +++ /dev/null @@ -1,543 +0,0 @@ -use std::collections::HashMap; - -use anyhow::Context; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, patch, post}, - Json, Router, -}; -use futures_util::future::join_all; -use helpers::handle_anyhow_error; -use mungos::mongodb::{ - bson::{doc, Document}, - options::FindOneOptions, -}; -use serde::{Deserialize, Serialize}; -use types::{ - traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState, - DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server, - TerminationSignal, UpdateStatus, -}; -use typeshare::typeshare; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -use super::spawn_request_action; - -#[derive(Serialize, Deserialize)] -pub struct DeploymentId { - id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CreateDeploymentBody { - name: String, - server_id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CopyDeploymentBody { - name: String, - server_id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct RenameDeploymentBody { - new_name: String, -} - -#[typeshare] -#[derive(Deserialize)] -pub struct GetContainerLogQuery { - tail: Option, -} - -#[typeshare] -#[derive(Deserialize)] -pub struct StopContainerQuery { - stop_signal: Option, - stop_time: Option, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id })| async move { - let res = state - .get_deployment_with_container_state(&user, &id) - .await - .map_err(handle_anyhow_error)?; - response!(Json(res)) - }, - ), - ) - .route( - "/list", - get( - |state: StateExtension, - user: RequestUserExtension, - Query(query): Query| async move { - let deployments = state - .list_deployments_with_container_state(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(deployments)) - }, - ), - ) - .route( - "/create", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(deployment): Json| async move { - let deployment = state - .create_deployment(&deployment.name, deployment.server_id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(deployment)) - }, - ), - ) - .route( - "/create_full", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(full_deployment): Json| async move { - let deployment = spawn_request_action(async move { - state - .create_full_deployment(full_deployment, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(deployment)) - }, - ), - ) - .route( - "/:id/copy", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }), - Json(deployment): Json| async move { - let deployment = spawn_request_action(async move { - state - .copy_deployment(&id, deployment.name, deployment.server_id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(deployment)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId{ id }), - Query(StopContainerQuery { stop_signal, stop_time })| async move { - let deployment = spawn_request_action(async move { - state - .delete_deployment(&id, &user, stop_signal, stop_time) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(deployment)) - }, - ), - ) - .route( - "/update", - patch( - |state: StateExtension, - user: RequestUserExtension, - Json(deployment): Json| async move { - let deployment = spawn_request_action(async move { - state - .update_deployment(deployment, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(deployment)) - }, - ), - ) - .route( - "/:id/rename", - patch( - |state: StateExtension, - user: RequestUserExtension, - deployment: Path, - body: Json| async move { - let update = spawn_request_action(async move { - state - .rename_deployment(&deployment.id, &body.new_name, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/reclone", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id })| async move { - let update = spawn_request_action(async move { - state - .reclone_deployment(&id, &user, true) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/deploy", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }), - Query(StopContainerQuery { stop_signal, stop_time })| async move { - let update = spawn_request_action(async move { - state - .deploy_container(&id, &user, stop_signal, stop_time) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/start_container", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id })| async move { - let update = spawn_request_action(async move { - state - .start_container(&id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/stop_container", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }), - Query(StopContainerQuery { stop_signal, stop_time })| async move { - let update = spawn_request_action(async move { - state - .stop_container(&id, &user, stop_signal, stop_time) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/remove_container", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }), - Query(StopContainerQuery { stop_signal, stop_time })| async move { - let update = spawn_request_action(async move { - state - .remove_container(&id, &user, stop_signal, stop_time) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/pull", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id })| async move { - let update = spawn_request_action(async move { - state - .pull_deployment_repo(&id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) - .route( - "/:id/action_state", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }): Path| async move { - let action_state = state - .get_deployment_action_states(id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(action_state)) - }, - ), - ) - .route( - "/:id/log", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id }), - Query(query): Query| async move { - let log = state - .get_deployment_container_log(&id, &user, query.tail) - .await - .map_err(handle_anyhow_error)?; - response!(Json(log)) - }, - ), - ) - .route( - "/:id/stats", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(DeploymentId { id })| async move { - let stats = state - .get_deployment_container_stats(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/deployed_version", - get( - |state: StateExtension, - 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 { - pub async fn get_deployment_with_container_state( - &self, - user: &RequestUser, - id: &str, - ) -> anyhow::Result { - let deployment = self - .get_deployment_check_permissions(id, user, PermissionLevel::Read) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let (state, container) = match self.periphery.container_list(&server).await { - Ok(containers) => match containers.into_iter().find(|c| c.name == deployment.name) { - Some(container) => (container.state, Some(container)), - None => (DockerContainerState::NotDeployed, None), - }, - Err(_) => (DockerContainerState::Unknown, None), - }; - Ok(DeploymentWithContainerState { - deployment, - state, - container, - }) - } - - async fn list_deployments_with_container_state( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let deployments: Vec = self - .db - .deployments - .get_some(query, None) - .await - .context("failed at get all deployments query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .collect(); - let mut servers: Vec = Vec::new(); - for d in &deployments { - if servers.iter().find(|s| s.id == d.server_id).is_none() { - servers.push(self.db.get_server(&d.server_id).await?) - } - } - let containers_futures = servers - .into_iter() - .map(|server| async { (self.periphery.container_list(&server).await, server.id) }); - - let containers = join_all(containers_futures) - .await - .into_iter() - .map(|(container, server_id)| (server_id, container.ok())) - .collect::>(); - let deployments_with_containers = deployments - .into_iter() - .map(|deployment| { - let (state, container) = match containers.get(&deployment.server_id).unwrap() { - Some(container) => { - match container - .iter() - .find(|c| c.name == deployment.name) - .map(|c| c.to_owned()) - { - Some(container) => (container.state, Some(container)), - None => (DockerContainerState::NotDeployed, None), - } - } - None => (DockerContainerState::Unknown, None), - }; - DeploymentWithContainerState { - container, - deployment, - state, - } - }) - .collect::>(); - Ok(deployments_with_containers) - } - - async fn get_deployment_action_states( - &self, - id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_deployment_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let action_state = self.deployment_action_states.get_or_default(id).await; - Ok(action_state) - } - - async fn get_deployment_container_log( - &self, - id: &str, - user: &RequestUser, - tail: Option, - ) -> anyhow::Result { - let deployment = self - .get_deployment_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let log = self - .periphery - .container_log(&server, &deployment.name, tail) - .await?; - Ok(log) - } - - async fn get_deployment_container_stats( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let deployment = self - .get_deployment_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let server = self.db.get_server(&deployment.server_id).await?; - let stats = self - .periphery - .container_stats(&server, &deployment.name) - .await?; - Ok(stats) - } - - async fn get_deployment_deployed_version( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result { - 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("unknown".to_string()) - } - } else { - Ok("unknown".to_string()) - } - } else { - let split = deployment - .docker_run_args - .image - .split(':') - .collect::>(); - if let Some(version) = split.get(1) { - Ok(version.to_string()) - } else { - Ok("unknown".to_string()) - } - } - } -} diff --git a/core/src/api/github_listener.rs b/core/src/api/github_listener.rs deleted file mode 100644 index 2e266ac86..000000000 --- a/core/src/api/github_listener.rs +++ /dev/null @@ -1,176 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{extract::Path, http::HeaderMap, routing::post, Router}; -use axum_oauth2::random_duration; -use helpers::handle_anyhow_error; -use hex::ToHex; -use hmac::{Hmac, Mac}; -use serde::Deserialize; -use sha2::Sha256; -use types::GITHUB_WEBHOOK_USER_ID; - -use crate::{ - auth::RequestUser, - state::{State, StateExtension}, -}; - -use super::spawn_request_action; - -type HmacSha256 = Hmac; - -#[derive(Deserialize)] -struct Id { - id: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/build/:id", - post(|state: StateExtension, Path(Id { id }), headers: HeaderMap, body: String| async move { - spawn_request_action(async move { - state.handle_build_webhook(&id, headers, body).await.map_err(handle_anyhow_error) - }).await? - }), - ) - .route( - "/deployment/:id", - post(|state: StateExtension, Path(Id { id }), headers: HeaderMap, body: String| async move { - spawn_request_action(async move { - state.handle_deployment_webhook(&id, headers, body).await.map_err(handle_anyhow_error) - }).await? - }), - ) - .route( - "/procedure/:id", - post(|state: StateExtension, Path(Id { id }), headers: HeaderMap, body: String| async move { - spawn_request_action(async move { - state.handle_procedure_webhook(&id, headers, body).await.map_err(handle_anyhow_error) - }).await? - }), - ) -} - -impl State { - async fn handle_build_webhook( - &self, - id: &str, - headers: HeaderMap, - body: String, - ) -> anyhow::Result<()> { - self.verify_gh_signature(headers, &body).await?; - let request_branch = extract_branch(&body)?; - let expected_branch = self - .db - .get_build(id) - .await? - .branch - .ok_or(anyhow!("build has no branch attached"))?; - if request_branch != expected_branch { - return Err(anyhow!("request branch does not match expected")); - } - self.build( - id, - &RequestUser { - id: String::from(GITHUB_WEBHOOK_USER_ID), - is_admin: true, - create_server_permissions: false, - create_build_permissions: false, - }, - ) - .await?; - Ok(()) - } - - async fn handle_deployment_webhook( - &self, - id: &str, - headers: HeaderMap, - body: String, - ) -> anyhow::Result<()> { - self.verify_gh_signature(headers, &body).await?; - let request_branch = extract_branch(&body)?; - let expected_branch = self - .db - .get_deployment(id) - .await? - .branch - .ok_or(anyhow!("deployment has no branch attached"))?; - if request_branch != expected_branch { - return Err(anyhow!("request branch does not match expected")); - } - self.pull_deployment_repo( - id, - &RequestUser { - id: String::from(GITHUB_WEBHOOK_USER_ID), - is_admin: true, - create_server_permissions: false, - create_build_permissions: false, - }, - ) - .await?; - Ok(()) - } - - async fn handle_procedure_webhook( - &self, - id: &str, - headers: HeaderMap, - body: String, - ) -> anyhow::Result<()> { - self.verify_gh_signature(headers, &body).await?; - let request_branch = extract_branch(&body)?; - let expected_branches = self.db.get_procedure(id).await?.webhook_branches; - if !expected_branches.contains(&request_branch) { - return Err(anyhow!("request branch does not match expected")); - } - self.run_procedure( - id, - &RequestUser { - id: String::from(GITHUB_WEBHOOK_USER_ID), - is_admin: true, - create_server_permissions: false, - create_build_permissions: false, - }, - ) - .await?; - Ok(()) - } - - async fn verify_gh_signature(&self, headers: HeaderMap, body: &str) -> anyhow::Result<()> { - // wait random amount of time - tokio::time::sleep(random_duration(0, 500)).await; - - let signature = headers.get("x-hub-signature-256"); - if signature.is_none() { - return Err(anyhow!("no signature in headers")); - } - let signature = signature.unwrap().to_str(); - if signature.is_err() { - return Err(anyhow!("failed to unwrap signature")); - } - let signature = signature.unwrap().replace("sha256=", ""); - let mut mac = HmacSha256::new_from_slice(self.config.github_webhook_secret.as_bytes()) - .expect("github webhook | failed to create hmac sha256"); - mac.update(body.as_bytes()); - let expected = mac.finalize().into_bytes().encode_hex::(); - if signature == expected { - Ok(()) - } else { - Err(anyhow!("signature does not equal expected")) - } - } -} - -#[derive(Deserialize)] -struct GithubWebhookBody { - #[serde(rename = "ref")] - branch: String, -} - -fn extract_branch(body: &str) -> anyhow::Result { - let branch = serde_json::from_str::(body) - .context("failed to parse github request body")? - .branch - .replace("refs/heads/", ""); - Ok(branch) -} diff --git a/core/src/api/group.rs b/core/src/api/group.rs deleted file mode 100644 index 1bb780b00..000000000 --- a/core/src/api/group.rs +++ /dev/null @@ -1,143 +0,0 @@ -use anyhow::Context; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, patch, post}, - Extension, Json, Router, -}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::Document; -use serde::{Deserialize, Serialize}; -use types::{traits::Permissioned, Group, PermissionLevel}; -use typeshare::typeshare; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -#[derive(Serialize, Deserialize)] -pub struct GroupId { - id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CreateGroupBody { - name: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(group_id): Path| async move { - let group = state - .get_group_check_permissions(&group_id.id, &user, PermissionLevel::Read) - .await - .map_err(handle_anyhow_error)?; - response!(Json(group)) - }, - ), - ) - .route( - "/list", - get( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Query(query): Query| async move { - let groups = state - .list_groups(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(groups)) - }, - ), - ) - .route( - "/create", - post( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(group): Json| async move { - let group = state - .create_group(&group.name, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(group)) - }, - ), - ) - .route( - "/create_full", - post( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(group): Json| async move { - let group = state - .create_full_group(group, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(group)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(group_id): Path| async move { - let group = state - .delete_group(&group_id.id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(group)) - }, - ), - ) - .route( - "/update", - patch( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(group): Json| async move { - let group = state - .update_group(group, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(group)) - }, - ), - ) -} - -impl State { - async fn list_groups( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let groups: Vec = self - .db - .groups - .get_some(query, None) - .await - .context("failed at get all groups query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .collect(); - // groups.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); - Ok(groups) - } -} diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs deleted file mode 100644 index 071dac471..000000000 --- a/core/src/api/mod.rs +++ /dev/null @@ -1,236 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{ - body::Body, - extract::Path, - http::{Request, StatusCode}, - middleware, - routing::{get, post}, - Extension, Json, Router, -}; -use futures_util::Future; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::doc; -use serde::Deserialize; -use types::{PermissionLevel, UpdateTarget, User}; -use typeshare::typeshare; - -use crate::{ - auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, - ResponseResult, -}; - -mod build; -mod command; -mod deployment; -mod github_listener; -mod group; -mod permissions; -mod procedure; -mod secret; -mod server; -mod update; - -#[typeshare] -#[derive(Deserialize)] -struct UpdateDescriptionBody { - target: UpdateTarget, - description: String, -} - -#[derive(Deserialize)] -struct UserId { - id: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/title", - get(|state: StateExtension| async move { state.config.title.clone() }), - ) - .route("/user", get(get_request_user)) - .nest("/listener", github_listener::router()) - .nest( - "/", - Router::new() - .route("/user/:id", get(get_user_at_id)) - .route( - "/username/:id", - get(|state: StateExtension, Path(UserId { id })| async move { - let user = state - .db - .get_user(&id) - .await - .context("failed to find user at id") - .map_err(handle_anyhow_error)?; - response!(Json(user.username)) - }), - ) - .route( - "/github_webhook_base_url", - get(|state: StateExtension| async move { - state - .config - .github_webhook_base_url - .as_ref() - .unwrap_or(&state.config.host) - .to_string() - }), - ) - .route( - "/update_description", - post( - |state: StateExtension, - user: RequestUserExtension, - body: Json| async move { - state - .update_description(&body.target, &body.description, &user) - .await - .map_err(handle_anyhow_error) - }, - ), - ) - .route("/users", get(get_users)) - .nest("/build", build::router()) - .nest("/deployment", deployment::router()) - .nest("/server", server::router()) - .nest("/command", command::router()) - .nest("/procedure", procedure::router()) - .nest("/group", group::router()) - .nest("/update", update::router()) - .nest("/permissions", permissions::router()) - .nest("/secret", secret::router()) - .layer(middleware::from_fn(auth_request)), - ) -} - -async fn get_request_user( - Extension(jwt): JwtExtension, - req: Request, -) -> ResponseResult> { - let mut user = jwt.authenticate(&req).await.map_err(handle_anyhow_error)?; - user.password = None; - for secret in &mut user.secrets { - secret.hash = String::new(); - } - Ok(Json(user)) -} - -async fn get_users( - state: StateExtension, - user: RequestUserExtension, -) -> ResponseResult>> { - if user.is_admin { - let users = state - .db - .users - .get_some(None, None) - .await - .context("failed to get users from db") - .map_err(handle_anyhow_error)? - .into_iter() - .map(|u| User { - password: None, - secrets: vec![], - ..u - }) - .collect::>(); - Ok(Json(users)) - } else { - Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string())) - } -} - -async fn get_user_at_id( - state: StateExtension, - Path(UserId { id }): Path, - user: RequestUserExtension, -) -> ResponseResult> { - if user.is_admin { - let mut user = state - .db - .users - .find_one_by_id(&id) - .await - .context("failed at query to get user from mongo") - .map_err(handle_anyhow_error)? - .ok_or(anyhow!("")) - .map_err(handle_anyhow_error)?; - user.password = None; - for secret in &mut user.secrets { - secret.hash = String::new(); - } - Ok(Json(user)) - } else { - Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string())) - } -} - -// need to run requested actions in here to prevent them being dropped mid action when user disconnects prematurely -pub async fn spawn_request_action(action: A) -> ResponseResult -where - A: Future + Send + 'static, - A::Output: Send + 'static, -{ - let res = tokio::spawn(action) - .await - .context("failure at action thread spawn") - .map_err(handle_anyhow_error)?; - Ok(res) -} - -impl State { - pub async fn update_description( - &self, - target: &UpdateTarget, - description: &str, - user: &RequestUser, - ) -> anyhow::Result<()> { - match target { - UpdateTarget::Build(id) => { - self.get_build_check_permissions(id, user, PermissionLevel::Update) - .await?; - self.db - .builds - .update_one::<()>(id, mungos::Update::Set(doc! { "description": description })) - .await?; - } - UpdateTarget::Deployment(id) => { - self.get_deployment_check_permissions(id, user, PermissionLevel::Update) - .await?; - self.db - .deployments - .update_one::<()>(id, mungos::Update::Set(doc! { "description": description })) - .await?; - } - UpdateTarget::Server(id) => { - self.get_server_check_permissions(id, user, PermissionLevel::Update) - .await?; - self.db - .servers - .update_one::<()>(id, mungos::Update::Set(doc! { "description": description })) - .await?; - } - UpdateTarget::Group(id) => { - self.get_group_check_permissions(id, user, PermissionLevel::Update) - .await?; - self.db - .groups - .update_one::<()>(id, mungos::Update::Set(doc! { "description": description })) - .await?; - } - UpdateTarget::Procedure(id) => { - self.get_procedure_check_permissions(id, user, PermissionLevel::Update) - .await?; - self.db - .procedures - .update_one::<()>(id, mungos::Update::Set(doc! { "description": description })) - .await?; - } - _ => return Err(anyhow!("invalid target: {target:?}")), - } - Ok(()) - } -} diff --git a/core/src/api/permissions.rs b/core/src/api/permissions.rs deleted file mode 100644 index 0dbda6417..000000000 --- a/core/src/api/permissions.rs +++ /dev/null @@ -1,410 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{routing::post, Extension, Json, Router}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::{doc, Document}; -use serde::{Deserialize, Serialize}; -use types::{ - monitor_timestamp, Build, Deployment, Group, Log, Operation, PermissionLevel, - PermissionsTarget, Procedure, Server, Update, UpdateStatus, UpdateTarget, -}; -use typeshare::typeshare; - -use crate::{auth::RequestUserExtension, response, state::StateExtension}; - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct PermissionsUpdateBody { - user_id: String, - permission: PermissionLevel, - target_type: PermissionsTarget, - target_id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct ModifyUserEnabledBody { - user_id: String, - enabled: bool, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct ModifyUserCreateServerBody { - user_id: String, - create_server_permissions: bool, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct ModifyUserCreateBuildBody { - user_id: String, - create_build_permissions: bool, -} - -pub fn router() -> Router { - Router::new() - .route( - "/update", - post(|state, user, update| async { - let update = update_permissions(state, user, update) - .await - .map_err(handle_anyhow_error)?; - response!(Json(update)) - }), - ) - .route( - "/modify_enabled", - post(|state, user, body| async { - let update = modify_user_enabled(state, user, body) - .await - .map_err(handle_anyhow_error)?; - response!(Json(update)) - }), - ) - .route( - "/modify_create_server", - post(|state, user, body| async { - let update = modify_user_create_server_permissions(state, user, body) - .await - .map_err(handle_anyhow_error)?; - 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( - Extension(state): StateExtension, - Extension(req_user): RequestUserExtension, - Json(permission_update): Json, -) -> anyhow::Result { - if !req_user.is_admin { - return Err(anyhow!( - "user not authorized for this action (is not admin)" - )); - } - let target_user = state - .db - .users - .find_one_by_id(&permission_update.user_id) - .await - .context("failed at find target user query")? - .ok_or(anyhow!( - "failed to find a user with id {}", - permission_update.user_id - ))?; - if !target_user.enabled { - return Err(anyhow!("target user not enabled")); - } - let mut update = Update { - operation: Operation::ModifyUserPermissions, - start_ts: monitor_timestamp(), - success: true, - operator: req_user.id.clone(), - status: UpdateStatus::Complete, - ..Default::default() - }; - let log_text = match permission_update.target_type { - PermissionsTarget::Server => { - let server = state - .db - .servers - .find_one_by_id(&permission_update.target_id) - .await - .context("failed at find server query")? - .ok_or(anyhow!( - "failed to find a server with id {}", - permission_update.target_id - ))?; - state - .db - .servers - .update_one::( - &permission_update.target_id, - mungos::Update::Set(doc! { - format!("permissions.{}", permission_update.user_id): permission_update.permission.to_string() - }), - ) - .await?; - update.target = UpdateTarget::Server(server.id); - format!( - "user {} given {} permissions on server {}", - target_user.username, permission_update.permission, server.name - ) - } - PermissionsTarget::Deployment => { - let deployment = state - .db - .deployments - .find_one_by_id(&permission_update.target_id) - .await - .context("failed at find deployment query")? - .ok_or(anyhow!( - "failed to find a deployment with id {}", - permission_update.target_id - ))?; - state - .db - .deployments - .update_one::( - &permission_update.target_id, - mungos::Update::Set(doc! { - format!("permissions.{}", permission_update.user_id): permission_update.permission.to_string() - }), - ) - .await?; - update.target = UpdateTarget::Deployment(deployment.id); - format!( - "user {} (id: {}) given {} permissions on deployment {}", - target_user.username, target_user.id, permission_update.permission, deployment.name - ) - } - PermissionsTarget::Build => { - let build = state - .db - .builds - .find_one_by_id(&permission_update.target_id) - .await - .context("failed at find build query")? - .ok_or(anyhow!( - "failed to find a build with id {}", - permission_update.target_id - ))?; - state - .db - .builds - .update_one::( - &permission_update.target_id, - mungos::Update::Set(doc! { - format!("permissions.{}", permission_update.user_id): permission_update.permission.to_string() - }), - ) - .await?; - update.target = UpdateTarget::Build(build.id); - format!( - "user {} given {} permissions on build {}", - target_user.username, permission_update.permission, build.name - ) - } - PermissionsTarget::Procedure => { - let procedure = state - .db - .procedures - .find_one_by_id(&permission_update.target_id) - .await - .context("failed at find procedure query")? - .ok_or(anyhow!( - "failed to find a procedure with id {}", - permission_update.target_id - ))?; - state - .db - .procedures - .update_one::( - &permission_update.target_id, - mungos::Update::Set(doc! { - format!("permissions.{}", permission_update.user_id): permission_update.permission.to_string() - }), - ) - .await?; - update.target = UpdateTarget::Procedure(procedure.id); - format!( - "user {} given {} permissions on procedure {}", - target_user.username, permission_update.permission, procedure.name - ) - } - PermissionsTarget::Group => { - let group = state - .db - .groups - .find_one_by_id(&permission_update.target_id) - .await - .context("failed at find group query")? - .ok_or(anyhow!( - "failed to find a group with id {}", - permission_update.target_id - ))?; - state - .db - .groups - .update_one::( - &permission_update.target_id, - mungos::Update::Set(doc! { - format!("permissions.{}", permission_update.user_id): permission_update.permission.to_string() - }), - ) - .await?; - update.target = UpdateTarget::Group(group.id); - format!( - "user {} given {} permissions on group {}", - target_user.username, permission_update.permission, group.name - ) - } - }; - update - .logs - .push(Log::simple("modify permissions", log_text)); - update.end_ts = Some(monitor_timestamp()); - update.id = state.add_update(update.clone()).await?; - Ok(update) -} - -async fn modify_user_enabled( - Extension(state): StateExtension, - Extension(req_user): RequestUserExtension, - Json(ModifyUserEnabledBody { user_id, enabled }): Json, -) -> anyhow::Result { - if !req_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::(&user_id, mungos::Update::Set(doc! { "enabled": enabled })) - .await?; - let update_type = if enabled { "enabled" } else { "disabled" }; - let ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::System, - operation: Operation::ModifyUserEnabled, - logs: vec![Log::simple( - "modify user enabled", - format!("{update_type} {} (id: {})", user.username, user.id), - )], - start_ts: ts.clone(), - end_ts: Some(ts), - status: UpdateStatus::Complete, - success: true, - operator: req_user.id.clone(), - ..Default::default() - }; - update.id = state.add_update(update.clone()).await?; - Ok(update) -} - -async fn modify_user_create_server_permissions( - Extension(state): StateExtension, - Extension(req_user): RequestUserExtension, - Json(ModifyUserCreateServerBody { - user_id, - create_server_permissions, - }): Json, -) -> anyhow::Result { - if !req_user.is_admin { - return Err(anyhow!( - "user does not have permissions for this action (not admin)" - )); - } - let target_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::( - &user_id, - mungos::Update::Set(doc! { "create_server_permissions": create_server_permissions }), - ) - .await?; - let update_type = if create_server_permissions { - "enabled" - } else { - "disabled" - }; - let ts = monitor_timestamp(); - let mut update = Update { - target: UpdateTarget::System, - operation: Operation::ModifyUserCreateServerPermissions, - logs: vec![Log::simple( - "modify user create server permissions", - format!( - "{update_type} create server permissions for {} (id: {})", - target_user.username, target_user.id - ), - )], - start_ts: ts.clone(), - end_ts: Some(ts), - status: UpdateStatus::Complete, - success: true, - operator: req_user.id.clone(), - ..Default::default() - }; - update.id = state.add_update(update.clone()).await?; - Ok(update) -} - -async fn modify_user_create_build_permissions( - Extension(state): StateExtension, - Extension(req_user): RequestUserExtension, - Json(ModifyUserCreateBuildBody { - user_id, - create_build_permissions, - }): Json, -) -> anyhow::Result { - if !req_user.is_admin { - return Err(anyhow!( - "user does not have permissions for this action (not admin)" - )); - } - let target_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::( - &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: {})", - target_user.username, target_user.id - ), - )], - start_ts: ts.clone(), - end_ts: Some(ts), - status: UpdateStatus::Complete, - success: true, - operator: req_user.id.clone(), - ..Default::default() - }; - update.id = state.add_update(update.clone()).await?; - Ok(update) -} diff --git a/core/src/api/procedure.rs b/core/src/api/procedure.rs deleted file mode 100644 index 9a4c400d9..000000000 --- a/core/src/api/procedure.rs +++ /dev/null @@ -1,166 +0,0 @@ -use anyhow::Context; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, patch, post}, - Extension, Json, Router, -}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::Document; -use serde::{Deserialize, Serialize}; -use types::{traits::Permissioned, PermissionLevel, Procedure}; -use typeshare::typeshare; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -use super::spawn_request_action; - -#[derive(Serialize, Deserialize)] -pub struct ProcedureId { - id: String, -} - -#[typeshare] -#[derive(Serialize, Deserialize)] -pub struct CreateProcedureBody { - name: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(procedure_id): Path| async move { - let procedure = state - .get_procedure_check_permissions( - &procedure_id.id, - &user, - PermissionLevel::Read, - ) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedure)) - }, - ), - ) - .route( - "/list", - get( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Query(query): Query| async move { - let procedures = state - .list_procedures(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedures)) - }, - ), - ) - .route( - "/create", - post( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(procedure): Json| async move { - let procedure = state - .create_procedure(&procedure.name, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedure)) - }, - ), - ) - .route( - "/create_full", - post( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(procedure): Json| async move { - let procedure = state - .create_full_procedure(procedure, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedure)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(procedure_id): Path| async move { - let procedure = state - .delete_procedure(&procedure_id.id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedure)) - }, - ), - ) - .route( - "/update", - patch( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Json(procedure): Json| async move { - let procedure = state - .update_procedure(procedure, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(procedure)) - }, - ), - ) - .route( - "/:id/run", - post( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(procedure_id): Path| async move { - let update = spawn_request_action(async move { - state - .run_procedure(&procedure_id.id, &user) - .await - .map_err(handle_anyhow_error) - }) - .await??; - response!(Json(update)) - }, - ), - ) -} - -impl State { - async fn list_procedures( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let procedures: Vec = self - .db - .procedures - .get_some(query, None) - .await - .context("failed at get all procedures query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .collect(); - // procedures.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); - Ok(procedures) - } -} diff --git a/core/src/api/secret.rs b/core/src/api/secret.rs deleted file mode 100644 index b1db226b4..000000000 --- a/core/src/api/secret.rs +++ /dev/null @@ -1,116 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{ - extract::Path, - routing::{delete, post}, - Extension, Json, Router, -}; -use helpers::{generate_secret, handle_anyhow_error}; -use mungos::{ - mongodb::bson::{doc, to_bson, Document}, - Update, -}; -use serde::{Deserialize, Serialize}; -use types::{monitor_timestamp, ApiSecret}; -use typeshare::typeshare; - -use crate::{auth::RequestUserExtension, state::StateExtension}; - -const SECRET_LENGTH: usize = 40; -const BCRYPT_COST: u32 = 10; - -#[typeshare] -#[derive(Serialize, Deserialize)] -struct CreateSecretBody { - name: String, - expires: Option, -} - -#[derive(Serialize, Deserialize)] -struct DeleteSecretPath { - name: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/create", - post(|state, user, secret| async { - create(state, user, secret) - .await - .map_err(handle_anyhow_error) - }), - ) - .route( - "/delete/:name", - delete(|state, user, secret_id| async { - delete_one(state, user, secret_id) - .await - .map_err(handle_anyhow_error) - }), - ) -} - -impl Into for CreateSecretBody { - fn into(self) -> ApiSecret { - ApiSecret { - name: self.name, - expires: self.expires, - created_at: monitor_timestamp(), - ..Default::default() - } - } -} - -async fn create( - Extension(state): StateExtension, - Extension(req_user): RequestUserExtension, - Json(secret): Json, -) -> anyhow::Result { - let user = state.db.get_user(&req_user.id).await?; - for s in &user.secrets { - if s.name == secret.name { - return Err(anyhow!("secret with name {} already exists", secret.name)); - } - } - let mut secret: ApiSecret = secret.into(); - let secret_str = generate_secret(SECRET_LENGTH); - secret.hash = - bcrypt::hash(&secret_str, BCRYPT_COST).context("failed at hashing secret string")?; - state - .db - .users - .update_one::( - &req_user.id, - Update::Custom(doc! { - "$push": { - "secrets": to_bson(&secret).context("failed at converting secret to bson")? - } - }), - ) - .await - .context("failed at mongo update query")?; - Ok(secret_str) -} - -async fn delete_one( - Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Path(DeleteSecretPath { name }): Path, -) -> anyhow::Result<()> { - state - .db - .users - .update_one::( - &user.id, - Update::Custom(doc! { - "$pull": { - "secrets": { - "name": name - } - } - }), - ) - .await - .context("failed at mongo update query")?; - Ok(()) -} diff --git a/core/src/api/server.rs b/core/src/api/server.rs deleted file mode 100644 index a879b1544..000000000 --- a/core/src/api/server.rs +++ /dev/null @@ -1,686 +0,0 @@ -use anyhow::{anyhow, Context}; -use async_timing_util::{get_timelength_in_ms, unix_timestamp_ms}; -use axum::{ - extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade}, - response::IntoResponse, - routing::{delete, get, patch, post}, - Json, Router, -}; -use futures_util::{future::join_all, SinkExt, StreamExt}; -use helpers::handle_anyhow_error; -use mungos::mongodb::{ - bson::{doc, Document}, - options::FindOptions, -}; -use serde::Deserialize; -use tokio::select; -use tokio_tungstenite::tungstenite::Message; -use tokio_util::sync::CancellationToken; -use types::{ - traits::Permissioned, BasicContainerInfo, HistoricalStatsQuery, ImageSummary, Network, - PermissionLevel, Server, ServerActionState, ServerStatus, ServerWithStatus, SystemInformation, - SystemStats, SystemStatsQuery, SystemStatsRecord, -}; -use typeshare::typeshare; - -const MAX_HISTORICAL_STATS_LIMIT: i64 = 500; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - response, - state::{State, StateExtension}, -}; - -use super::spawn_request_action; - -#[derive(Deserialize)] -struct ServerId { - id: String, -} - -#[derive(Deserialize)] -struct Ts { - ts: i64, -} - -#[typeshare] -#[derive(Deserialize)] -pub struct CreateServerBody { - name: String, - address: String, -} - -pub fn router() -> Router { - Router::new() - .route( - "/:id", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(server_id): Path| async move { - let server = state - .get_server(&server_id.id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(server)) - }, - ), - ) - .route( - "/list", - get( - |state: StateExtension, - user: RequestUserExtension, - Query(query): Query| async move { - let servers = state - .list_servers(&user, query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(servers)) - }, - ), - ) - .route( - "/create", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(server): Json| async move { - let server = state - .create_server(&server.name, server.address, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(server)) - }, - ), - ) - .route( - "/create_full", - post( - |state: StateExtension, - user: RequestUserExtension, - Json(server): Json| async move { - let server = state - .create_full_server(server, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(server)) - }, - ), - ) - .route( - "/:id/delete", - delete( - |state: StateExtension, - user: RequestUserExtension, - Path(server): Path| async move { - let server = state - .delete_server(&server.id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(server)) - }, - ), - ) - .route( - "/update", - patch( - |state: StateExtension, - user: RequestUserExtension, - Json(server): Json| async move { - let server = state - .update_server(server, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(server)) - }, - ), - ) - .route( - "/:id/version", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = state - .get_server_version(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/system_information", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = state - .get_server_system_info(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/stats", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id }), - query: Query| async move { - let stats = state - .get_server_stats(&id, &user, &query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/stats/history", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id }), - query: Query| async move { - let stats = state - .get_historical_stats(&id, &user, &query) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/stats/at_ts", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id }), - Query(Ts { ts })| async move { - let stats = state - .get_stats_at_ts(&id, &user, ts) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/stats/ws", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id }), - Query(query): Query, - ws: WebSocketUpgrade| async move { - let connection = state - .subscribe_to_stats_ws(&id, &user, &query, ws) - .await - .map_err(handle_anyhow_error)?; - response!(connection) - }, - ), - ) - .route( - "/:id/networks", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = state - .get_networks(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/networks/prune", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = spawn_request_action(async move { - state - .prune_networks(&id, &user) - .await - .map_err(handle_anyhow_error) - }).await??; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/images", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = state - .get_images(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/images/prune", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = spawn_request_action(async move { - state - .prune_images(&id, &user) - .await - .map_err(handle_anyhow_error) - }).await??; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/containers", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = state - .get_containers(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/containers/prune", - post( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let stats = spawn_request_action(async move { - state - .prune_containers(&id, &user) - .await - .map_err(handle_anyhow_error) - }).await??; - response!(Json(stats)) - }, - ), - ) - .route( - "/:id/github_accounts", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let github_accounts = state - .get_github_accounts(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(github_accounts)) - }, - ), - ) - .route( - "/:id/docker_accounts", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let docker_accounts = state - .get_docker_accounts(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(docker_accounts)) - }, - ), - ) - .route( - "/:id/secrets", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let vars = state - .get_available_secrets(&id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(vars)) - }, - ), - ) - .route( - "/:id/action_state", - get( - |state: StateExtension, - user: RequestUserExtension, - Path(ServerId { id })| async move { - let action_state = state - .get_server_action_states(id, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(action_state)) - }, - ), - ) -} - -impl State { - pub async fn get_server( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(id, user, PermissionLevel::Read) - .await?; - let status = if server.enabled { - let res = self.periphery.health_check(&server).await; - match res { - Ok(_) => ServerStatus::Ok, - Err(_) => ServerStatus::NotOk, - } - } else { - ServerStatus::Disabled - }; - Ok(ServerWithStatus { server, status }) - } - - async fn list_servers( - &self, - user: &RequestUser, - query: impl Into>, - ) -> anyhow::Result> { - let futures = self - .db - .servers - .get_some(query, None) - .await - .context("failed at get all servers query")? - .into_iter() - .filter(|s| { - if user.is_admin { - true - } else { - let permissions = s.get_user_permissions(&user.id); - permissions != PermissionLevel::None - } - }) - .map(|server| async { - let status = if server.enabled { - let res = self.periphery.health_check(&server).await; - match res { - Ok(_) => ServerStatus::Ok, - Err(_) => ServerStatus::NotOk, - } - } else { - ServerStatus::Disabled - }; - - ServerWithStatus { server, status } - }); - Ok(join_all(futures).await) - } - - async fn get_server_version( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let version = self.periphery.get_version(&server).await.context(format!( - "failed to get system information from server {}", - server.name - ))?; - Ok(version) - } - - async fn get_server_system_info( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let stats = self - .periphery - .get_system_information(&server) - .await - .context(format!( - "failed to get system information from server {}", - server.name - ))?; - Ok(stats) - } - - async fn get_server_stats( - &self, - server_id: &str, - user: &RequestUser, - query: &SystemStatsQuery, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let stats = self - .periphery - .get_system_stats(&server, query) - .await - .context(format!("failed to get stats from server {}", server.name))?; - Ok(stats) - } - - async fn get_historical_stats( - &self, - server_id: &str, - user: &RequestUser, - query: &HistoricalStatsQuery, - ) -> anyhow::Result> { - self.get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - - let mut projection = doc! { "processes": 0, "disk.disks": 0 }; - if !query.networks { - projection.insert("networks", 0); - } - if !query.components { - projection.insert("components", 0); - } - let limit = if query.limit as i64 > MAX_HISTORICAL_STATS_LIMIT { - MAX_HISTORICAL_STATS_LIMIT - } else { - query.limit as i64 - }; - let interval = get_timelength_in_ms(query.interval.to_string().parse().unwrap()) as i64; - let mut ts_vec = Vec::::new(); - let curr_ts = unix_timestamp_ms() as i64; - let mut curr_ts = curr_ts - curr_ts % interval - interval * limit * query.page as i64; - for _ in 0..limit { - ts_vec.push(curr_ts); - curr_ts -= interval; - } - self.db - .stats - .get_some( - doc! { - "server_id": server_id, - "ts": { "$in": ts_vec } - }, - FindOptions::builder() - .sort(doc! { "ts": 1 }) - .projection(projection) - .build(), - ) - .await - .context("failed at mongo query to get stats") - } - - async fn get_stats_at_ts( - &self, - server_id: &str, - user: &RequestUser, - ts: i64, - ) -> anyhow::Result { - self.get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - self.db - .stats - .find_one(doc! { "server_id": server_id, "ts": ts }, None) - .await - .context("failed at mongo query to get full stat entry")? - .ok_or(anyhow!("did not find entry for server at time")) - } - - async fn subscribe_to_stats_ws( - &self, - server_id: &str, - user: &RequestUser, - query: &SystemStatsQuery, - ws: WebSocketUpgrade, - ) -> anyhow::Result { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let mut stats_reciever = self.periphery.subscribe_to_stats_ws(&server, query).await?; - let upgrade = ws.on_upgrade(|socket| async move { - let (mut ws_sender, mut ws_recv) = socket.split(); - let cancel = CancellationToken::new(); - let cancel_clone = cancel.clone(); - tokio::spawn(async move { - loop { - let stats = select! { - _ = cancel_clone.cancelled() => break, - stats = stats_reciever.next() => stats - }; - if let Some(Ok(Message::Text(msg))) = stats { - let _ = ws_sender.send(AxumMessage::Text(msg)).await; - } - } - }); - while let Some(msg) = ws_recv.next().await { - match msg { - Ok(msg) => match msg { - AxumMessage::Close(_) => { - cancel.cancel(); - return; - } - _ => {} - }, - Err(_) => { - cancel.cancel(); - return; - } - } - } - }); - Ok(upgrade) - } - - async fn get_networks( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let stats = self.periphery.network_list(&server).await.context(format!( - "failed to get networks from server {}", - server.name - ))?; - Ok(stats) - } - - async fn get_images( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let images = self - .periphery - .image_list(&server) - .await - .context(format!("failed to get images from server {}", server.name))?; - Ok(images) - } - - async fn get_containers( - &self, - server_id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(server_id, user, PermissionLevel::Read) - .await?; - let containers = self - .periphery - .container_list(&server) - .await - .context(format!( - "failed to get containers from server {}", - server.name - ))?; - Ok(containers) - } - - async fn get_github_accounts( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(id, user, PermissionLevel::Read) - .await?; - let github_accounts = self.periphery.get_github_accounts(&server).await?; - Ok(github_accounts) - } - - async fn get_docker_accounts( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(id, user, PermissionLevel::Read) - .await?; - let docker_accounts = self.periphery.get_docker_accounts(&server).await?; - Ok(docker_accounts) - } - - async fn get_available_secrets( - &self, - id: &str, - user: &RequestUser, - ) -> anyhow::Result> { - let server = self - .get_server_check_permissions(id, user, PermissionLevel::Read) - .await?; - let vars = self.periphery.get_available_secrets(&server).await?; - Ok(vars) - } - - async fn get_server_action_states( - &self, - id: String, - user: &RequestUser, - ) -> anyhow::Result { - self.get_server_check_permissions(&id, &user, PermissionLevel::Read) - .await?; - let action_state = self.server_action_states.get_or_default(id).await; - Ok(action_state) - } -} diff --git a/core/src/api/update.rs b/core/src/api/update.rs deleted file mode 100644 index 5b4fe11b5..000000000 --- a/core/src/api/update.rs +++ /dev/null @@ -1,223 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{extract::Query, routing::get, Extension, Json, Router}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::{doc, to_bson}; -use serde_json::Value; -use types::{PermissionLevel, Update, UpdateTarget}; - -use crate::{ - auth::{RequestUser, RequestUserExtension}, - helpers::parse_comma_seperated_list, - response, - state::{State, StateExtension}, -}; - -const NUM_UPDATES_PER_PAGE: usize = 20; - -pub fn router() -> Router { - Router::new().route( - "/list", - get( - |Extension(state): StateExtension, - Extension(user): RequestUserExtension, - Query(value): Query| async move { - let offset = value - .get("offset") - .map(|v| v.as_str().unwrap_or("0").parse().unwrap_or(0)) - .unwrap_or(0); - let target = serde_json::from_str::(&value.to_string()).ok(); - let show_builds = value - .get("show_builds") - .map(|b| { - b.as_str() - .unwrap_or("false") - .parse::() - .unwrap_or_default() - }) - .unwrap_or_default(); - let operations = value - .get("operations") - .map(|o| { - let o = o.as_str().unwrap_or_default(); - if o.len() == 0 { - return None; - } - parse_comma_seperated_list::(o).ok() - }) - .flatten(); - let updates = state - .list_updates(target, offset, show_builds, operations, &user) - .await - .map_err(handle_anyhow_error)?; - response!(Json(updates)) - }, - ), - ) -} - -impl State { - async fn permission_on_update_target( - &self, - update_target: &UpdateTarget, - user: &RequestUser, - ) -> anyhow::Result<()> { - if user.is_admin { - Ok(()) - } else { - match update_target { - UpdateTarget::System => { - if user.is_admin { - Ok(()) - } else { - Err(anyhow!("user must be admin to see system updates")) - } - } - UpdateTarget::Build(id) => self - .get_build_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - UpdateTarget::Deployment(id) => self - .get_deployment_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - UpdateTarget::Server(id) => self - .get_server_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - UpdateTarget::Procedure(id) => self - .get_procedure_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - UpdateTarget::Group(id) => self - .get_group_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - UpdateTarget::Command(id) => self - .get_command_check_permissions(id, user, PermissionLevel::Read) - .await - .map(|_| ()), - } - } - } - - pub async fn list_updates( - &self, - target: Option, - offset: u64, - show_builds: bool, - operations: Option>, - user: &RequestUser, - ) -> anyhow::Result> { - let mut filter = match target { - Some(target) => { - if let (UpdateTarget::Deployment(id), true) = (&target, show_builds) { - let deployment = self - .get_deployment_check_permissions(id, user, PermissionLevel::Read) - .await?; - if let Some(build_id) = &deployment.build_id { - let build = self - .get_build_check_permissions(build_id, user, PermissionLevel::Read) - .await; - if let Ok(_) = build { - doc! { - "$or": [ - {"target": to_bson(&target).unwrap()}, - {"target": { "type": "Build", "id": build_id }, "operation": "build_build"} - ], - } - } else { - doc! { - "target": to_bson(&target).unwrap() - } - } - } else { - doc! { - "target": to_bson(&target).unwrap() - } - } - } else { - self.permission_on_update_target(&target, user).await?; - doc! { - "target": to_bson(&target).unwrap() - } - } - } - None => { - if user.is_admin { - doc! {} - } else { - let permissions_field = format!("permissions.{}", user.id); - let target_filter = doc! { - "$or": [ - { &permissions_field: "update" }, - { &permissions_field: "execute" }, - { &permissions_field: "read" }, - ] - }; - let build_ids = self - .db - .builds - .get_some(target_filter.clone(), None) - .await - .context("failed at query to get users builds")? - .into_iter() - .map(|e| e.id) - .collect::>(); - let deployment_ids = self - .db - .deployments - .get_some(target_filter.clone(), None) - .await - .context("failed at query to get users deployments")? - .into_iter() - .map(|e| e.id) - .collect::>(); - let server_ids = self - .db - .servers - .get_some(target_filter.clone(), None) - .await - .context("failed at query to get users servers")? - .into_iter() - .map(|e| e.id) - .collect::>(); - let procedure_ids = self - .db - .procedures - .get_some(target_filter, None) - .await - .context("failed at query to get users procedures")? - .into_iter() - .map(|e| e.id) - .collect::>(); - let filter = doc! { - "$or": [ - { "target.type": "Build", "target.id": { "$in": &build_ids } }, - { "target.type": "Deployment", "target.id": { "$in": &deployment_ids } }, - { "target.type": "Server", "target.id": { "$in": &server_ids } }, - { "target.type": "Procedure", "target.id": { "$in": &procedure_ids } } - ] - }; - filter - } - } - }; - if let Some(operations) = operations { - filter.insert("operation", doc! { "$in": operations }); - } - let mut updates = self - .db - .updates - .get_most_recent( - "start_ts", - NUM_UPDATES_PER_PAGE as i64, - offset, - filter, - None, - ) - .await - .context("mongo get most recent updates query failed")?; - updates.reverse(); - Ok(updates) - } -} diff --git a/core/src/auth/github.rs b/core/src/auth/github.rs deleted file mode 100644 index cd3ea10f0..000000000 --- a/core/src/auth/github.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::sync::Arc; - -use anyhow::{anyhow, Context}; -use axum::{extract::Query, response::Redirect, routing::get, Extension, Router}; -use axum_oauth2::github::{GithubOauthClient, GithubOauthExtension}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::doc; -use serde::Deserialize; -use types::{monitor_timestamp, CoreConfig, User}; - -use crate::{response, state::StateExtension}; - -use super::JwtExtension; - -pub fn router(config: &CoreConfig) -> Router { - let client = GithubOauthClient::new( - config.github_oauth.id.clone(), - config.github_oauth.secret.clone(), - format!("{}/auth/github/callback", config.host), - &[], - "monitor".to_string(), - ); - Router::new() - .route( - "/login", - get(|Extension(client): GithubOauthExtension| async move { - Redirect::to(&client.get_login_redirect_url()) - }), - ) - .route( - "/callback", - get(|client, jwt, state, query| async { - let redirect = callback(client, jwt, state, query) - .await - .map_err(handle_anyhow_error)?; - response!(redirect) - }), - ) - .layer(Extension(Arc::new(client))) -} - -#[derive(Deserialize)] -struct CallbackQuery { - state: String, - code: String, -} - -async fn callback( - Extension(client): GithubOauthExtension, - Extension(jwt_client): JwtExtension, - Extension(state): StateExtension, - Query(query): Query, -) -> anyhow::Result { - if !client.check_state(&query.state) { - return Err(anyhow!("state mismatch")); - } - let token = client.get_access_token(&query.code).await?; - let github_user = client.get_github_user(&token.access_token).await?; - let github_id = github_user.id.to_string(); - let user = state - .db - .users - .find_one(doc! { "github_id": &github_id }, None) - .await - .context("failed at find user query from mongo")?; - let jwt = match user { - Some(user) => jwt_client - .generate(user.id) - .context("failed to generate jwt")?, - None => { - let ts = monitor_timestamp(); - let no_users_exist = state.db.users.find_one(None, None).await?.is_none(); - let user = User { - username: github_user.login, - avatar: github_user.avatar_url.into(), - github_id: github_id.into(), - enabled: no_users_exist, - admin: no_users_exist, - create_server_permissions: no_users_exist, - create_build_permissions: no_users_exist, - created_at: ts.clone(), - updated_at: ts, - ..Default::default() - }; - let user_id = state - .db - .users - .create_one(user) - .await - .context("failed to create user on mongo")?; - jwt_client - .generate(user_id) - .context("failed to generate jwt")? - } - }; - let exchange_token = jwt_client.create_exchange_token(jwt); - Ok(Redirect::to(&format!( - "{}?token={exchange_token}", - state.config.host - ))) -} diff --git a/core/src/auth/google.rs b/core/src/auth/google.rs deleted file mode 100644 index a00cb461f..000000000 --- a/core/src/auth/google.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::sync::Arc; - -use anyhow::{anyhow, Context}; -use axum::{extract::Query, response::Redirect, routing::get, Extension, Router}; -use axum_oauth2::google::{GoogleOauthClient, GoogleOauthExtension}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::doc; -use serde::Deserialize; -use types::{monitor_timestamp, CoreConfig, User}; - -use crate::{response, state::StateExtension}; - -use super::JwtExtension; - -pub fn router(config: &CoreConfig) -> Router { - let client = GoogleOauthClient::new( - config.google_oauth.id.clone(), - config.google_oauth.secret.clone(), - format!("{}/auth/google/callback", config.host), - &[ - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email", - ], - "monitor".to_string(), - ); - Router::new() - .route( - "/login", - get(|Extension(client): GoogleOauthExtension| async move { - Redirect::to(&client.get_login_redirect_url()) - }), - ) - .route( - "/callback", - get(|client, jwt, state, query| async { - let redirect = callback(client, jwt, state, query) - .await - .map_err(handle_anyhow_error)?; - response!(redirect) - }), - ) - .layer(Extension(Arc::new(client))) -} - -#[derive(Deserialize)] -struct CallbackQuery { - state: Option, - code: Option, - error: Option, -} - -async fn callback( - Extension(client): GoogleOauthExtension, - Extension(jwt_client): JwtExtension, - Extension(state): StateExtension, - Query(query): Query, -) -> anyhow::Result { - if let Some(error) = query.error { - return Err(anyhow!("auth error from google: {error}")); - } - if !client.check_state( - &query - .state - .ok_or(anyhow!("callback query does not contain state"))?, - ) { - return Err(anyhow!("state mismatch")); - } - let token = client - .get_access_token( - &query - .code - .ok_or(anyhow!("callback query does not contain code"))?, - ) - .await?; - let google_user = client.get_google_user(&token.id_token)?; - let google_id = google_user.id.to_string(); - let user = state - .db - .users - .find_one(doc! { "google_id": &google_id }, None) - .await - .context("failed at find user query from mongo")?; - let jwt = match user { - Some(user) => jwt_client - .generate(user.id) - .context("failed to generate jwt")?, - None => { - let ts = monitor_timestamp(); - let no_users_exist = state.db.users.find_one(None, None).await?.is_none(); - let user = User { - username: google_user - .email - .split("@") - .collect::>() - .get(0) - .unwrap() - .to_string(), - avatar: google_user.picture.into(), - google_id: google_id.into(), - enabled: no_users_exist, - admin: no_users_exist, - create_server_permissions: no_users_exist, - create_build_permissions: no_users_exist, - created_at: ts.clone(), - updated_at: ts, - ..Default::default() - }; - let user_id = state - .db - .users - .create_one(user) - .await - .context("failed to create user on mongo")?; - jwt_client - .generate(user_id) - .context("failed to generate jwt")? - } - }; - let exchange_token = jwt_client.create_exchange_token(jwt); - Ok(Redirect::to(&format!( - "{}?token={exchange_token}", - state.config.host - ))) -} diff --git a/core/src/auth/jwt.rs b/core/src/auth/jwt.rs deleted file mode 100644 index aeb07405d..000000000 --- a/core/src/auth/jwt.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -use anyhow::{anyhow, Context}; -use async_timing_util::{get_timelength_in_ms, unix_timestamp_ms, Timelength}; -use axum::{body::Body, http::Request, Extension}; -use axum_oauth2::random_string; -use hmac::{Hmac, Mac}; -use jwt::{SignWithKey, VerifyWithKey}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use types::{CoreConfig, User}; - -use crate::state::State; - -pub type JwtExtension = Extension>; -pub type RequestUserExtension = Extension>; - -type ExchangeTokenMap = Mutex>; - -#[derive(Default)] -pub struct RequestUser { - pub id: String, - pub is_admin: bool, - pub create_server_permissions: bool, - pub create_build_permissions: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct JwtClaims { - pub id: String, - pub iat: u128, - pub exp: u128, -} - -pub struct JwtClient { - key: Hmac, - valid_for_ms: u128, - exchange_tokens: ExchangeTokenMap, -} - -impl JwtClient { - pub fn extension(config: &CoreConfig) -> JwtExtension { - let key = Hmac::new_from_slice(config.jwt_secret.as_bytes()) - .expect("failed at taking HmacSha256 of jwt secret"); - let client = JwtClient { - key, - valid_for_ms: get_timelength_in_ms(config.jwt_valid_for.to_string().parse().unwrap()), - exchange_tokens: Default::default(), - }; - Extension(Arc::new(client)) - } - - pub fn generate(&self, user_id: String) -> anyhow::Result { - let iat = unix_timestamp_ms(); - let exp = iat + self.valid_for_ms; - let claims = JwtClaims { - id: user_id, - iat, - exp, - }; - let jwt = claims - .sign_with_key(&self.key) - .context("failed at signing claim")?; - Ok(jwt) - } - - pub async fn authenticate_check_enabled( - &self, - req: &Request, - ) -> anyhow::Result> { - let jwt = req - .headers() - .get("authorization") - .ok_or(anyhow!( - "no authorization header provided. must be Bearer " - ))? - .to_str()? - .replace("Bearer ", "") - .replace("bearer ", ""); - let state = req - .extensions() - .get::>() - .ok_or(anyhow!("failed at getting state handle"))?; - let user = self - .auth_jwt_check_enabled(&jwt, &state) - .await - .context("failed to authenticate jwt")?; - Ok(Arc::new(user)) - } - - pub async fn auth_jwt_check_enabled( - &self, - jwt: &str, - state: &State, - ) -> anyhow::Result { - let claims: JwtClaims = jwt - .verify_with_key(&self.key) - .context("failed to verify claims")?; - if claims.exp > unix_timestamp_ms() { - let user = state - .db - .users - .find_one_by_id(&claims.id) - .await? - .ok_or(anyhow!("did not find user with id {}", claims.id))?; - if user.enabled { - let user = RequestUser { - id: claims.id, - is_admin: user.admin, - create_server_permissions: user.create_server_permissions, - create_build_permissions: user.create_build_permissions, - }; - Ok(user) - } else { - Err(anyhow!("user not enabled")) - } - } else { - Err(anyhow!("token has expired")) - } - } - - pub async fn authenticate(&self, req: &Request) -> anyhow::Result { - let jwt = req - .headers() - .get("authorization") - .ok_or(anyhow!( - "no authorization header provided. must be Bearer " - ))? - .to_str()? - .replace("Bearer ", "") - .replace("bearer ", ""); - let state = req - .extensions() - .get::>() - .ok_or(anyhow!("failed at getting state handle"))?; - let user = self - .auth_jwt(&jwt, &state) - .await - .context("failed to authenticate jwt")?; - Ok(user) - } - - pub async fn auth_jwt(&self, jwt: &str, state: &State) -> anyhow::Result { - let claims: JwtClaims = jwt - .verify_with_key(&self.key) - .context("failed to verify claims")?; - if claims.exp > unix_timestamp_ms() { - let user = state - .db - .users - .find_one_by_id(&claims.id) - .await? - .ok_or(anyhow!("did not find user with id {}", claims.id))?; - Ok(user) - } else { - Err(anyhow!("token has expired")) - } - } - - pub fn create_exchange_token(&self, jwt: String) -> String { - let exchange_token = random_string(40); - self.exchange_tokens.lock().unwrap().insert( - exchange_token.clone(), - ( - jwt, - unix_timestamp_ms() + get_timelength_in_ms(Timelength::OneMinute), - ), - ); - exchange_token - } - - pub fn redeem_exchange_token(&self, exchange_token: &str) -> anyhow::Result { - let (jwt, valid_until) = self - .exchange_tokens - .lock() - .unwrap() - .remove(exchange_token) - .ok_or(anyhow!("invalid exchange token: unrecognized"))?; - if unix_timestamp_ms() < valid_until { - Ok(jwt) - } else { - Err(anyhow!("invalid exchange token: expired")) - } - } -} diff --git a/core/src/auth/local.rs b/core/src/auth/local.rs deleted file mode 100644 index ff2c4d57a..000000000 --- a/core/src/auth/local.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::{anyhow, Context}; -use axum::{extract::Json, routing::post, Extension, Router}; -use helpers::handle_anyhow_error; -use mungos::mongodb::bson::doc; -use types::{monitor_timestamp, User, UserCredentials}; - -use crate::state::StateExtension; - -use super::jwt::JwtExtension; - -const BCRYPT_COST: u32 = 10; - -pub fn router() -> Router { - Router::new() - .route( - "/create_user", - post(|db, jwt, body| async { - create_user_handler(db, jwt, body) - .await - .map_err(handle_anyhow_error) - }), - ) - .route( - "/login", - post(|db, jwt, body| async { - login_handler(db, jwt, body) - .await - .map_err(handle_anyhow_error) - }), - ) -} - -async fn create_user_handler( - Extension(state): StateExtension, - Extension(jwt): JwtExtension, - Json(UserCredentials { username, password }): Json, -) -> anyhow::Result { - let password = bcrypt::hash(password, BCRYPT_COST).context("failed to hash password")?; - - let no_users_exist = state.db.users.find_one(None, None).await?.is_none(); - - let ts = monitor_timestamp(); - - let user = User { - username, - password: Some(password), - enabled: no_users_exist, - admin: no_users_exist, - create_server_permissions: no_users_exist, - create_build_permissions: no_users_exist, - created_at: ts.clone(), - updated_at: ts, - ..Default::default() - }; - - let user_id = state - .db - .users - .create_one(user) - .await - .context("failed to create user")?; - - let jwt = jwt - .generate(user_id) - .context("failed to generate jwt for user")?; - - Ok(jwt) -} - -async fn login_handler( - Extension(state): StateExtension, - Extension(jwt): JwtExtension, - Json(UserCredentials { username, password }): Json, -) -> anyhow::Result { - let user = state - .db - .users - .find_one(doc! { "username": &username }, None) - .await - .context("failed at mongo query")? - .ok_or(anyhow!("did not find user with username {username}"))?; - - let user_pw_hash = user - .password - .ok_or(anyhow!("invalid login, user does not have password login"))?; - - let verified = bcrypt::verify(password, &user_pw_hash).context("failed at verify password")?; - - if !verified { - return Err(anyhow!("invalid credentials")); - } - - let jwt = jwt - .generate(user.id) - .context("failed at generating jwt for user")?; - - Ok(jwt) -} diff --git a/core/src/auth/mod.rs b/core/src/auth/mod.rs deleted file mode 100644 index cd8001c1a..000000000 --- a/core/src/auth/mod.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::sync::Arc; - -use axum::{ - body::Body, - http::{Request, StatusCode}, - middleware::Next, - response::Response, - routing::{get, post}, - Extension, Json, Router, -}; -use helpers::handle_anyhow_error; -use serde::{Deserialize, Serialize}; -use types::CoreConfig; -use typeshare::typeshare; - -mod github; -mod google; -mod jwt; -mod local; -mod secret; - -use crate::state::StateExtension; - -pub use self::jwt::{JwtClaims, JwtClient, JwtExtension, RequestUser, RequestUserExtension}; - -#[typeshare] -#[derive(Serialize)] -struct LoginOptions { - local: bool, - github: bool, - google: bool, -} - -pub fn router(config: &CoreConfig) -> Router { - let mut router = Router::new() - .route( - "/options", - get(|Extension(state): StateExtension| async move { - Json(LoginOptions { - local: state.config.local_auth, - github: state.config.github_oauth.enabled - && state.config.github_oauth.id.len() > 0 - && state.config.github_oauth.secret.len() > 0, - google: state.config.google_oauth.enabled - && state.config.google_oauth.id.len() > 0 - && state.config.google_oauth.secret.len() > 0, - }) - }), - ) - .route( - "/exchange", - post(|jwt, body| async { - exchange_for_jwt(jwt, body) - .await - .map_err(handle_anyhow_error) - }), - ) - .nest("/secret", secret::router()); - - if config.local_auth { - router = router.nest("/local", local::router()); - } - - if config.github_oauth.enabled - && config.github_oauth.id.len() > 0 - && config.github_oauth.secret.len() > 0 - { - router = router.nest("/github", github::router(config)); - } - - if config.google_oauth.enabled - && config.google_oauth.id.len() > 0 - && config.google_oauth.secret.len() > 0 - { - router = router.nest("/google", google::router(config)); - } - - router -} - -#[typeshare] -#[derive(Deserialize)] -struct TokenExchangeBody { - token: String, -} - -async fn exchange_for_jwt( - Extension(jwt): JwtExtension, - Json(body): Json, -) -> anyhow::Result { - let jwt = jwt.redeem_exchange_token(&body.token)?; - Ok(jwt) -} - -pub async fn auth_request( - mut req: Request, - next: Next, -) -> Result { - let jwt_client = req.extensions().get::>().ok_or(( - StatusCode::UNAUTHORIZED, - "failed to get jwt client extension".to_string(), - ))?; - let user = jwt_client - .authenticate_check_enabled(&req) - .await - .map_err(|e| (StatusCode::UNAUTHORIZED, format!("{e:#?}")))?; - req.extensions_mut().insert(user); - Ok(next.run(req).await) -} diff --git a/core/src/auth/secret.rs b/core/src/auth/secret.rs deleted file mode 100644 index 24d5c1908..000000000 --- a/core/src/auth/secret.rs +++ /dev/null @@ -1,66 +0,0 @@ -use anyhow::{anyhow, Context}; -use async_timing_util::unix_timestamp_ms; -use axum::{routing::post, Extension, Json, Router}; -use helpers::handle_anyhow_error; -use mungos::{ - mongodb::bson::{doc, Document}, - Update, -}; -use serde::Deserialize; -use types::unix_from_monitor_ts; - -use crate::state::StateExtension; - -use super::JwtExtension; - -#[derive(Deserialize)] -pub struct SecretLoginBody { - username: String, - secret: String, -} - -pub fn router() -> Router { - Router::new().route( - "/login", - post(|db, jwt, body| async { login(db, jwt, body).await.map_err(handle_anyhow_error) }), - ) -} - -pub async fn login( - Extension(state): StateExtension, - Extension(jwt): JwtExtension, - Json(SecretLoginBody { username, secret }): Json, -) -> anyhow::Result { - let user = state - .db - .users - .find_one(doc! { "username": &username }, None) - .await - .context("failed at mongo query")? - .ok_or(anyhow!("did not find user with username {username}"))?; - let ts = unix_timestamp_ms() as i64; - for s in user.secrets { - if let Some(expires) = s.expires { - let expires = unix_from_monitor_ts(&expires)?; - if expires < ts { - state - .db - .users - .update_one::( - &user.id, - Update::Custom(doc! { "$pull": { "secrets": { "name": s.name } } }), - ) - .await - .context("failed to remove expired secret")?; - continue; - } - } - if bcrypt::verify(&secret, &s.hash).context("failed at verifying hash")? { - let jwt = jwt - .generate(user.id) - .context("failed at generating jwt for user")?; - return Ok(jwt); - } - } - Err(anyhow!("invalid secret")) -} diff --git a/core/src/cloud/aws.rs b/core/src/cloud/aws.rs deleted file mode 100644 index f802ef7f3..000000000 --- a/core/src/cloud/aws.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::time::Duration; - -use anyhow::{anyhow, Context}; -use aws_sdk_ec2::{ - config::Region, - types::{ - BlockDeviceMapping, EbsBlockDevice, InstanceNetworkInterfaceSpecification, - InstanceStateChange, InstanceStateName, InstanceStatus, InstanceType, ResourceType, Tag, - TagSpecification, - }, - Client, -}; -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, - volume_size_gb: i32, - key_pair_name: &str, - assign_public_ip: bool, -) -> anyhow::Result { - 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> { - 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> { - 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 { - 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 { - 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) -} diff --git a/core/src/cloud/mod.rs b/core/src/cloud/mod.rs deleted file mode 100644 index 5e5eeaa3d..000000000 --- a/core/src/cloud/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod aws; diff --git a/core/src/config.rs b/core/src/config.rs deleted file mode 100644 index e6bbccdb5..000000000 --- a/core/src/config.rs +++ /dev/null @@ -1,37 +0,0 @@ -use axum::Router; -use dotenv::dotenv; -use merge_config_files::parse_config_file; -use serde::Deserialize; -use tower_http::services::{ServeDir, ServeFile}; -use types::CoreConfig; - -type SpaRouter = Router; - -#[derive(Deserialize, Debug)] -struct Env { - #[serde(default = "default_config_path")] - pub config_path: String, - #[serde(default = "default_frontend_path")] - pub frontend_path: String, -} - -pub fn load() -> (CoreConfig, SpaRouter, ServeFile) { - dotenv().ok(); - let env: Env = envy::from_env().expect("failed to parse environment variables"); - let config = parse_config_file(env.config_path).expect("failed to parse config"); - let spa_router = Router::new().nest_service( - "/assets", - ServeDir::new(&env.frontend_path) - .not_found_service(ServeFile::new(format!("{}/index.html", env.frontend_path))), - ); - let index_html_service = ServeFile::new(format!("{}/index.html", env.frontend_path)); - (config, spa_router, index_html_service) -} - -pub fn default_config_path() -> String { - "/config/config.toml".to_string() -} - -fn default_frontend_path() -> String { - "/frontend".to_string() -} diff --git a/core/src/helpers.rs b/core/src/helpers.rs deleted file mode 100644 index 9e9632778..000000000 --- a/core/src/helpers.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use anyhow::anyhow; -use diff::{Diff, OptionDiff}; -use helpers::to_monitor_name; -use tokio::sync::RwLock; -use types::{traits::Busy, Build}; - -#[macro_export] -macro_rules! response { - ($x:expr) => { - Ok::<_, (axum::http::StatusCode, String)>($x) - }; -} - -pub fn option_diff_is_some(diff: &OptionDiff) -> bool -where - ::Repr: PartialEq, -{ - diff != &OptionDiff::NoChange && diff != &OptionDiff::None -} - -pub fn any_option_diff_is_some(diffs: &[&OptionDiff]) -> bool -where - ::Repr: PartialEq, -{ - for diff in diffs { - if diff != &&OptionDiff::NoChange && diff != &&OptionDiff::None { - return true; - } - } - return false; -} - -pub fn parse_comma_seperated_list(comma_sep_list: &str) -> anyhow::Result> { - comma_sep_list - .split(",") - .filter(|item| item.len() > 0) - .map(|item| { - let item = item - .parse() - .map_err(|_| anyhow!("error parsing string {item} into type T"))?; - Ok::(item) - }) - .collect() -} - -pub fn get_image_name(build: &Build) -> String { - let name = to_monitor_name(&build.name); - match &build.docker_organization { - Some(org) => format!("{org}/{name}"), - None => match &build.docker_account { - Some(acct) => format!("{acct}/{name}"), - None => name, - }, - } -} - -pub fn empty_or_only_spaces(word: &str) -> bool { - if word.len() == 0 { - return true; - } - for char in word.chars() { - if char != ' ' { - return false; - } - } - return true; -} - -#[derive(Default)] -pub struct Cache { - cache: RwLock>, -} - -impl Cache { - pub async fn get(&self, key: &str) -> Option { - self.cache.read().await.get(key).map(|e| e.clone()) - } - - pub async fn get_or_default(&self, key: String) -> T { - let mut cache = self.cache.write().await; - cache.entry(key).or_default().clone() - } - - pub async fn update_entry(&self, key: String, handler: impl Fn(&mut T) -> ()) { - let mut cache = self.cache.write().await; - handler(cache.entry(key).or_default()); - } - - pub async fn clear(&self) { - self.cache.write().await.clear(); - } -} - -impl Cache { - pub async fn busy(&self, id: &str) -> bool { - match self.get(id).await { - Some(state) => state.busy(), - None => false, - } - } -} diff --git a/core/src/main.rs b/core/src/main.rs deleted file mode 100644 index d2f86478e..000000000 --- a/core/src/main.rs +++ /dev/null @@ -1,63 +0,0 @@ -// #![allow(unused)] - -use ::helpers::get_socket_addr; -use auth::JwtClient; -use axum::{http::StatusCode, Router}; -use state::State; -use termination_signal::tokio::immediate_term_handle; -use tower_http::cors::{Any, CorsLayer}; - -mod actions; -mod api; -mod auth; -mod cloud; -mod config; -mod helpers; -mod monitoring; -mod state; -mod ws; - -type ResponseResult = Result; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - println!("version: v{}", env!("CARGO_PKG_VERSION")); - - let term_signal = immediate_term_handle()?; - - let app = tokio::spawn(async move { - let (config, spa_router, index_html_service) = config::load(); - - println!("starting monitor core on port {}...", config.port); - - let app = Router::new() - .nest("/api", api::router()) - .nest("/auth", auth::router(&config)) - .nest("/ws", ws::router()) - .layer(JwtClient::extension(&config)) - .layer(State::extension(config.clone()).await) - .merge(spa_router) - .fallback_service(index_html_service) - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any), - ); - - println!("started monitor core on port {}", config.port); - - axum::Server::bind(&get_socket_addr(config.port)) - .serve(app.into_make_service()) - .await?; - - anyhow::Ok(()) - }); - - tokio::select! { - res = app => return res?, - _ = term_signal => {}, - } - - Ok(()) -} diff --git a/core/src/monitoring.rs b/core/src/monitoring.rs deleted file mode 100644 index 34ad11426..000000000 --- a/core/src/monitoring.rs +++ /dev/null @@ -1,537 +0,0 @@ -use std::{cmp::Ordering, collections::HashMap, path::PathBuf}; - -use async_timing_util::{ - unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS, ONE_HOUR_MS, -}; -use futures_util::future::join_all; -use mungos::mongodb::bson::doc; -use slack::types::Block; -use types::{Server, SystemStats, SystemStatsQuery, SystemStatsRecord}; - -use crate::state::State; - -#[derive(Default, Clone)] -pub struct AlertStatus { - cpu_alert: bool, - mem_alert: bool, - disk_alert: HashMap, - component_alert: bool, -} - -impl State { - pub async fn collect_server_stats(&self) { - loop { - let ts = wait_until_timelength( - self.config.monitoring_interval.to_string().parse().unwrap(), - 0, - ) - .await as i64; - let servers = self.get_enabled_servers_with_stats().await; - if let Err(e) = servers { - eprintln!("failed to get server list from db: {e:?}"); - continue; - } - for (server, res) in servers.unwrap() { - if let Err(_) = res { - if let Some(slack) = &self.slack { - let (header, info) = generate_unreachable_message(&server); - let res = slack.send_message_with_header(&header, info.clone()).await; - if let Err(e) = res { - eprintln!("failed to send message to slack: {e} | header: {header} | info: {info:?}") - } - } - continue; - } - let stats = res.unwrap(); - self.check_server_stats(&server, &stats).await; - let res = self - .db - .stats - .create_one(SystemStatsRecord::from_stats(server.id, ts, stats)) - .await; - if let Err(e) = res { - eprintln!("failed to insert stats into mongo | {e}"); - } - } - } - } - - pub async fn prune_stats_on_mongo(&self) { - let days_ago_ms = self.config.keep_stats_for_days as u128 * ONE_DAY_MS; - loop { - let ts = wait_until_timelength(Timelength::OneDay, 0).await; - let delete_before_ts = ts - days_ago_ms; - let res = self - .db - .stats - .delete_many(doc! { "ts": { "$lte": delete_before_ts as i64 } }) - .await; - if let Err(e) = res { - eprintln!("{ts} | failed to delete old stats | {e:?}"); - } - } - } - - async fn get_enabled_servers_with_stats( - &self, - ) -> anyhow::Result)>> { - let servers = self - .db - .servers - .get_some(doc! { "enabled": true }, None) - .await?; - - let futures = servers.into_iter().map(|server| async move { - let stats = self - .periphery - .get_system_stats(&server, &SystemStatsQuery::all()) - .await; - (server, stats) - }); - - Ok(join_all(futures).await) - } - - async fn check_server_stats(&self, server: &Server, stats: &SystemStats) { - self.check_cpu(server, stats).await; - self.check_mem(server, stats).await; - self.check_disk(server, stats).await; - self.check_components(server, stats).await; - } - - async fn check_cpu(&self, server: &Server, stats: &SystemStats) { - if self.slack.is_none() - || self - .server_alert_status - .get(&server.id) - .await - .map(|s| s.cpu_alert) - .unwrap_or(false) - { - return; - } - if stats.cpu_perc > server.cpu_alert { - let region = if let Some(region) = &server.region { - format!(" ({region})") - } else { - String::new() - }; - let mut top_procs = stats.processes.clone(); - top_procs.sort_by(|a, b| { - if a.cpu_perc > b.cpu_perc { - Ordering::Less - } else { - Ordering::Greater - } - }); - let top_procs = top_procs - .into_iter() - .take(3) - .enumerate() - .map(|(i, p)| { - format!( - "\n{}. *{}* | *{:.1}%* CPU | *{:.1} GiB* MEM", - i + 1, - p.name, - p.cpu_perc, - p.mem_mb / 1024.0, - ) - }) - .collect::>() - .join(""); - let mut blocks = vec![ - Block::header("WARNING 🚨"), - Block::section(format!( - "*{}*{region} has high *CPU usage* 📈 🚨", - server.name - )), - Block::section(format!("cpu: *{:.1}%*", stats.cpu_perc)), - Block::section(format!("*top cpu processes*{top_procs}",)), - ]; - - if let Some(to_notify) = generate_to_notify(server) { - blocks.push(Block::section(to_notify)) - } - - let res = self - .slack - .as_ref() - .unwrap() - .send_message( - format!( - "WARNING 🚨 | *{}*{region} has high *CPU usage* 📈 🚨", - server.name - ), - blocks, - ) - .await; - if let Err(e) = res { - eprintln!( - "failed to send message to slack | high cpu usage on {} | usage: {:.1}% | {e:?}", - server.name, stats.cpu_perc - ) - } else { - self.server_alert_status - .update_entry(server.id.clone(), |entry| { - entry.cpu_alert = true; - }) - .await; - } - } - } - - async fn check_mem(&self, server: &Server, stats: &SystemStats) { - if self.slack.is_none() - || self - .server_alert_status - .get(&server.id) - .await - .map(|s| s.mem_alert) - .unwrap_or(false) - { - return; - } - let usage_perc = (stats.mem_used_gb / stats.mem_total_gb) * 100.0; - if usage_perc > server.mem_alert { - let region = if let Some(region) = &server.region { - format!(" ({region})") - } else { - String::new() - }; - let mut top_procs = stats.processes.clone(); - top_procs.sort_by(|a, b| { - if a.mem_mb > b.mem_mb { - Ordering::Less - } else { - Ordering::Greater - } - }); - let top_procs = top_procs - .into_iter() - .take(3) - .enumerate() - .map(|(i, p)| { - format!( - "\n{}. *{}* | *{:.1}%* CPU | *{:.1} GiB* MEM", - i + 1, - p.name, - p.cpu_perc, - p.mem_mb / 1024.0, - ) - }) - .collect::>() - .join(""); - let mut blocks = vec![ - Block::header("WARNING 🚨"), - Block::section(format!( - "*{}*{region} has high *memory usage* 💾 🚨", - server.name - )), - Block::section(format!( - "memory: used *{:.2} GB* of *{:.2} GB* (*{:.1}%*)", - stats.mem_used_gb, stats.mem_total_gb, usage_perc - )), - Block::section(format!("*top mem processes*{top_procs}",)), - ]; - - if let Some(to_notify) = generate_to_notify(server) { - blocks.push(Block::section(to_notify)) - } - - let res = self - .slack - .as_ref() - .unwrap() - .send_message( - format!( - "WARNING 🚨 | *{}*{region} has high *memory usage* 💾 🚨", - server.name - ), - blocks, - ) - .await; - if let Err(e) = res { - eprintln!( - "failed to send message to slack | high mem usage on {} | usage: {:.2}GB of {:.2}GB | {e:?}", - server.name, stats.mem_used_gb, stats.mem_total_gb, - ) - } else { - self.server_alert_status - .update_entry(server.id.clone(), |entry| { - entry.mem_alert = true; - }) - .await; - } - } - } - - async fn check_disk(&self, server: &Server, stats: &SystemStats) { - for disk in &stats.disk.disks { - if self.slack.is_none() - || self - .server_alert_status - .get(&server.id) - .await - .map(|s| *s.disk_alert.get(&disk.mount).unwrap_or(&false)) - .unwrap_or(false) - { - return; - } - let usage_perc = (disk.used_gb / disk.total_gb) * 100.0; - if usage_perc > server.disk_alert { - let region = if let Some(region) = &server.region { - format!(" ({region})") - } else { - String::new() - }; - let mut blocks = vec![ - Block::header("WARNING 🚨"), - Block::section(format!( - "*{}*{region} has high *disk usage* (mount point *{}*) 💿 🚨", - server.name, - disk.mount.display() - )), - Block::section(format!( - "disk: used *{:.2} GB* of *{:.2} GB* (*{:.1}%*)", - disk.used_gb, disk.total_gb, usage_perc - )), - ]; - - if let Some(to_notify) = generate_to_notify(server) { - blocks.push(Block::section(to_notify)) - } - - let res = self - .slack - .as_ref() - .unwrap() - .send_message( - format!( - "WARNING 🚨 | *{}*{region} has high *disk usage* 💿 🚨", - server.name - ), - blocks, - ) - .await; - if let Err(e) = res { - eprintln!( - "failed to send message to slack | high disk usage on {} | usage: {:.2}GB of {:.2}GB | {e:?}", - server.name, stats.disk.used_gb, stats.disk.total_gb, - ) - } else { - self.server_alert_status - .update_entry(server.id.clone(), |entry| { - entry.disk_alert.insert(disk.mount.clone(), true); - }) - .await; - } - } - } - } - - async fn check_components(&self, server: &Server, stats: &SystemStats) { - if self.slack.is_none() - || self - .server_alert_status - .get(&server.id) - .await - .map(|s| s.component_alert) - .unwrap_or(false) - { - return; - } - let info = stats - .components - .iter() - .map(|c| { - if let Some(critical) = c.critical { - if c.temp / critical > 0.85 { - format!( - "{}: *{:.1}°* (*{:.1}%* to critical) 🌡️", - c.label, - c.temp, - (c.temp / critical) * 100.0 - ) - } else { - String::new() - } - } else { - String::new() - } - }) - .filter(|s| !s.is_empty()) - .collect::>(); - if info.len() > 0 { - let region = if let Some(region) = &server.region { - format!(" ({region})") - } else { - String::new() - }; - let mut blocks = vec![ - Block::header("WARNING 🚨"), - Block::section(format!( - "*{}*{region} has high *tempurature* 🌡️ 🚨", - server.name - )), - Block::section(info.join("\n")), - ]; - - if let Some(to_notify) = generate_to_notify(server) { - blocks.push(Block::section(to_notify)) - } - - let res = self - .slack - .as_ref() - .unwrap() - .send_message( - format!( - "WARNING 🚨 | *{}*{region} has high *tempurature* 🌡️ 🚨", - server.name - ), - blocks, - ) - .await; - if let Err(e) = res { - eprintln!( - "failed to send message to slack | high tempurature on {} | {} | {e:?}", - server.name, - info.join(" | "), - ) - } else { - self.server_alert_status - .update_entry(server.id.clone(), |entry| { - entry.component_alert = true; - }) - .await; - } - } - } - - pub async fn daily_update(&self) { - let offset = self.config.daily_offset_hours as u128 * ONE_HOUR_MS; - loop { - wait_until_timelength(Timelength::OneDay, offset).await; - let servers = self.get_enabled_servers_with_stats().await; - if let Err(e) = &servers { - eprintln!( - "{} | failed to get servers with stats for daily update | {e:#?}", - unix_timestamp_ms() - ); - continue; - } - let servers = servers.unwrap(); - if servers.is_empty() { - continue; - } - let mut blocks = vec![Block::header("INFO | daily update"), Block::divider()]; - for (server, stats) in servers { - let region = if let Some(region) = &server.region { - format!(" | {region}") - } else { - String::new() - }; - if let Ok(stats) = stats { - let cpu_warning = if stats.cpu_perc > server.cpu_alert { - " 🚨" - } else { - "" - }; - let mem_warning = - if (stats.mem_used_gb / stats.mem_total_gb) * 100.0 > server.mem_alert { - " 🚨" - } else { - "" - }; - let disk_warning = - if (stats.disk.used_gb / stats.disk.total_gb) * 100.0 > server.disk_alert { - " 🚨" - } else { - "" - }; - let status = if !cpu_warning.is_empty() - || !mem_warning.is_empty() - || !disk_warning.is_empty() - { - "*WARNING* 🚨" - } else { - "*OK* ✅" - }; - let name_line = format!("*{}*{region} | {status}", server.name); - let cpu_line = format!("CPU: *{:.1}%*{cpu_warning}", stats.cpu_perc); - let mem_line = format!( - "MEM: *{:.1}%* ({:.2} GB of {:.2} GB){mem_warning}", - (stats.mem_used_gb / stats.mem_total_gb) * 100.0, - stats.mem_used_gb, - stats.mem_total_gb, - ); - let disk_line = format!( - "DISK: *{:.1}%* ({:.2} GB of {:.2} GB){disk_warning}", - (stats.disk.used_gb / stats.disk.total_gb) * 100.0, - stats.disk.used_gb, - stats.disk.total_gb, - ); - blocks.push(Block::section(format!( - "{name_line}\n{cpu_line}\n{mem_line}\n{disk_line}", - ))); - } else { - blocks.push(Block::section(format!( - "*{}*{region} | *UNREACHABLE* ❌", - server.name - ))); - } - blocks.push(Block::divider()) - } - let res = self - .slack - .as_ref() - .unwrap() - .send_message(format!("INFO | daily update"), blocks) - .await; - if let Err(e) = res { - eprintln!( - "{} | failed to send daily update message | {e:?}", - unix_timestamp_ms() - ); - } - { - self.server_alert_status.clear().await; - } - } - } -} - -fn generate_unreachable_message(server: &Server) -> (String, Option) { - let region = match &server.region { - Some(region) => format!(" ({region})"), - None => String::new(), - }; - let header = format!("WARNING 🚨 | {}{region} is unreachable ❌", server.name); - let to_notify = server - .to_notify - .iter() - .map(|u| format!("<@{u}>")) - .collect::>() - .join(" "); - let info = if to_notify.len() > 0 { - Some(to_notify) - } else { - None - }; - (header, info) -} - -fn generate_to_notify(server: &Server) -> Option { - if server.to_notify.len() > 0 { - Some( - server - .to_notify - .iter() - .map(|u| format!("<@{u}>")) - .collect::>() - .join(" "), - ) - } else { - None - } -} diff --git a/core/src/state.rs b/core/src/state.rs deleted file mode 100644 index 844a6b1c8..000000000 --- a/core/src/state.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::sync::Arc; - -use async_timing_util::{unix_timestamp_ms, wait_until_timelength, Timelength, ONE_HOUR_MS}; -use axum::Extension; -use db::DbClient; -use futures_util::future::join_all; -use mungos::mongodb::bson::doc; -use periphery::PeripheryClient; -use types::{ - BuildActionState, CommandActionState, CoreConfig, DeploymentActionState, ServerActionState, -}; - -use crate::{helpers::Cache, monitoring::AlertStatus, ws::update::UpdateWsChannel}; - -pub type StateExtension = Extension>; - -// pub type Cache = RwLock>; -pub struct State { - pub config: CoreConfig, - pub db: DbClient, - pub update: UpdateWsChannel, - pub periphery: PeripheryClient, - pub slack: Option, - pub build_action_states: Cache, - pub deployment_action_states: Cache, - pub server_action_states: Cache, - pub command_action_states: Cache, - pub server_alert_status: Cache, // (server_id, AlertStatus) -} - -impl State { - pub async fn new(config: CoreConfig) -> Arc { - 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(), - build_action_states: Default::default(), - deployment_action_states: Default::default(), - server_action_states: Default::default(), - command_action_states: Default::default(), - server_alert_status: Default::default(), - }; - let state = Arc::new(state); - let state_clone = state.clone(); - tokio::spawn(async move { state_clone.collect_server_stats().await }); - let state_clone = state.clone(); - tokio::spawn(async move { state_clone.daily_image_prune().await }); - if state.slack.is_some() { - let state_clone = state.clone(); - tokio::spawn(async move { state_clone.daily_update().await }); - } - if state.config.keep_stats_for_days != 0 { - let state_clone = state.clone(); - tokio::spawn(async move { state_clone.prune_stats_on_mongo().await }); - } - state - } - - pub async fn extension(config: CoreConfig) -> StateExtension { - Extension(State::new(config).await) - } - - async fn daily_image_prune(&self) { - let offset = self.config.daily_offset_hours as u128 * ONE_HOUR_MS; - loop { - wait_until_timelength(Timelength::OneDay, offset).await; - let servers = self - .db - .servers - .get_some(doc! { "enabled": true, "auto_prune": true }, None) - .await; - if let Err(e) = &servers { - eprintln!( - "{} | failed to get servers for daily prune | {e:#?}", - unix_timestamp_ms() - ); - continue; - } - let futures = servers.unwrap().into_iter().map(|server| async move { - let _ = self.periphery.image_prune(&server).await; - }); - join_all(futures).await; - } - } -} diff --git a/core/src/ws/mod.rs b/core/src/ws/mod.rs deleted file mode 100644 index d8b352bae..000000000 --- a/core/src/ws/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -use axum::{ - extract::ws::{Message, WebSocket}, - routing::get, - Router, -}; - -use crate::{ - auth::{JwtClient, RequestUser}, - state::State, -}; - -mod stats; -pub mod update; - -pub fn router() -> Router { - Router::new() - .route("/update", get(update::ws_handler)) - .route("/stats/:id", get(stats::ws_handler)) -} - -impl State { - pub async fn ws_login( - &self, - mut socket: WebSocket, - jwt_client: &JwtClient, - ) -> Option<(WebSocket, RequestUser)> { - if let Some(jwt) = socket.recv().await { - match jwt { - Ok(jwt) => match jwt { - Message::Text(jwt) => { - match jwt_client.auth_jwt_check_enabled(&jwt, self).await { - Ok(user) => { - let _ = socket.send(Message::Text("LOGGED_IN".to_string())).await; - Some((socket, user)) - } - Err(e) => { - let _ = socket - .send(Message::Text(format!( - "failed to authenticate user | {e:#?}" - ))) - .await; - let _ = socket.close().await; - None - } - } - } - msg => { - let _ = socket - .send(Message::Text(format!("invalid login msg: {msg:#?}"))) - .await; - let _ = socket.close().await; - None - } - }, - Err(e) => { - let _ = socket - .send(Message::Text(format!("failed to get jwt message: {e:#?}"))) - .await; - let _ = socket.close().await; - None - } - } - } else { - let _ = socket - .send(Message::Text(format!("failed to get jwt message"))) - .await; - let _ = socket.close().await; - None - } - } -} diff --git a/core/src/ws/stats.rs b/core/src/ws/stats.rs deleted file mode 100644 index 2026e7ae7..000000000 --- a/core/src/ws/stats.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade}, - response::IntoResponse, -}; -use futures_util::{SinkExt, StreamExt}; -use helpers::handle_anyhow_error; -use serde::Deserialize; -use tokio::select; -use tokio_tungstenite::tungstenite::Message as TungsteniteMessage; -use tokio_util::sync::CancellationToken; -use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery}; - -use crate::{auth::JwtExtension, state::StateExtension, ResponseResult}; - -#[derive(Deserialize)] -pub struct ServerId { - id: String, -} - -pub async fn ws_handler( - state: StateExtension, - jwt_client: JwtExtension, - path: Path, - query: Query, - ws: WebSocketUpgrade, -) -> ResponseResult { - let server = state - .db - .get_server(&path.id) - .await - .map_err(handle_anyhow_error)?; - let query = Arc::new(query); - let upgrade = ws.on_upgrade(|socket| async move { - let login_res = state.ws_login(socket, &jwt_client).await; - if login_res.is_none() { - return; - } - let (mut socket, user) = login_res.unwrap(); - if !user.is_admin && server.get_user_permissions(&user.id) < PermissionLevel::Read { - let _ = socket - .send(AxumMessage::Text( - "permission denied. user must have at least read permissions on this server" - .to_string(), - )) - .await; - return; - } - let (mut ws_sender, mut ws_reciever) = socket.split(); - let res = state.periphery.subscribe_to_stats_ws(&server, &query).await; - if let Err(e) = &res { - let _ = ws_sender - .send(AxumMessage::Text(format!("ERROR: {e}"))) - .await; - return; - } - let mut stats_recv = res.unwrap(); - let cancel = CancellationToken::new(); - let cancel_clone = cancel.clone(); - tokio::spawn(async move { - loop { - let stats = select! { - _ = cancel_clone.cancelled() => { - let _ = stats_recv.close(None).await; - break - }, - stats = stats_recv.next() => stats, - }; - if let Some(Ok(TungsteniteMessage::Text(msg))) = stats { - let _ = ws_sender.send(AxumMessage::Text(msg)).await; - } else { - let _ = stats_recv.close(None).await; - break; - } - } - }); - while let Some(msg) = ws_reciever.next().await { - match msg { - Ok(msg) => match msg { - AxumMessage::Close(_) => { - // println!("CLOSE"); - cancel.cancel(); - return; - } - _ => {} - }, - Err(_) => { - // println!("CLOSE FROM ERR"); - cancel.cancel(); - return; - } - } - } - }); - Ok(upgrade) -} diff --git a/core/src/ws/update.rs b/core/src/ws/update.rs deleted file mode 100644 index 8b5bab3d5..000000000 --- a/core/src/ws/update.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anyhow::anyhow; -use axum::{ - extract::{ws::Message, WebSocketUpgrade}, - response::IntoResponse, -}; -use db::DbClient; -use futures_util::{SinkExt, StreamExt}; -use serde_json::json; -use tokio::{ - select, - sync::{ - broadcast::{self, Receiver, Sender}, - Mutex, - }, -}; -use tokio_util::sync::CancellationToken; -use types::{PermissionLevel, Update, UpdateTarget, User}; - -use crate::{auth::JwtExtension, state::StateExtension}; - -pub type UpdateWsSender = Mutex>; - -pub type UpdateWsReciever = Receiver; -pub struct UpdateWsChannel { - pub sender: UpdateWsSender, - pub reciever: UpdateWsReciever, -} - -impl UpdateWsChannel { - pub fn new() -> UpdateWsChannel { - let (sender, reciever) = broadcast::channel(16); - UpdateWsChannel { - sender: Mutex::new(sender), - reciever, - } - } -} - -pub async fn ws_handler( - jwt_client: JwtExtension, - state: StateExtension, - ws: WebSocketUpgrade, -) -> impl IntoResponse { - let mut reciever = state.update.reciever.resubscribe(); - ws.on_upgrade(|socket| async move { - let login_res = state.ws_login(socket, &jwt_client).await; - if login_res.is_none() { - return; - } - let (socket, user) = login_res.unwrap(); - let (mut ws_sender, mut ws_reciever) = socket.split(); - let cancel = CancellationToken::new(); - let cancel_clone = cancel.clone(); - tokio::spawn(async move { - loop { - let update = select! { - _ = cancel_clone.cancelled() => break, - update = reciever.recv() => {update.expect("failed to recv update msg")} - }; - let user = state.db.users.find_one_by_id(&user.id).await; - if user.is_err() - || user.as_ref().unwrap().is_none() - || !user.as_ref().unwrap().as_ref().unwrap().enabled - { - let _ = ws_sender - .send(Message::Text(json!({ "type": "INVALID_USER" }).to_string())) - .await; - let _ = ws_sender.close().await; - return; - } - let user = user.unwrap().unwrap(); // already handle cases where this panics in the above early return - match user_can_see_update(&user, &user.id, &update.target, &state.db).await { - Ok(_) => { - let _ = ws_sender - .send(Message::Text(serde_json::to_string(&update).unwrap())) - .await; - } - Err(_) => { - // make these error visible in some way - } - } - } - }); - - while let Some(msg) = ws_reciever.next().await { - match msg { - Ok(msg) => match msg { - Message::Close(_) => { - cancel.cancel(); - return; - } - _ => {} - }, - Err(_) => { - cancel.cancel(); - return; - } - } - } - }) -} - -async fn user_can_see_update( - user: &User, - user_id: &str, - update_target: &UpdateTarget, - db_client: &DbClient, -) -> anyhow::Result<()> { - if user.admin { - return Ok(()); - } - let (permissions, target) = match update_target { - UpdateTarget::Server(server_id) => { - let permissions = db_client - .get_user_permission_on_server(user_id, server_id) - .await?; - (permissions, "server") - } - UpdateTarget::Deployment(deployment_id) => { - let permissions = db_client - .get_user_permission_on_deployment(user_id, deployment_id) - .await?; - (permissions, "deployment") - } - UpdateTarget::Build(build_id) => { - let permissions = db_client - .get_user_permission_on_build(user_id, build_id) - .await?; - (permissions, "build") - } - UpdateTarget::Procedure(procedure_id) => { - let permissions = db_client - .get_user_permission_on_procedure(user_id, procedure_id) - .await?; - (permissions, "procedure") - } - UpdateTarget::Group(group_id) => { - let permissions = db_client - .get_user_permission_on_group(user_id, group_id) - .await?; - (permissions, "group") - } - UpdateTarget::Command(command_id) => { - let permissions = db_client - .get_user_permission_on_command(user_id, command_id) - .await?; - (permissions, "command") - } - UpdateTarget::System => { - return Err(anyhow!("user not admin, can't recieve system updates")) - } - }; - if permissions != PermissionLevel::None { - Ok(()) - } else { - Err(anyhow!("user does not have permissions on {target}")) - } -} diff --git a/docsite/.gitignore b/docsite/.gitignore deleted file mode 100644 index b2d6de306..000000000 --- a/docsite/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/docsite/README.md b/docsite/README.md deleted file mode 100644 index aaba2fa1e..000000000 --- a/docsite/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Website - -This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -Using SSH: - -``` -$ USE_SSH=true yarn deploy -``` - -Not using SSH: - -``` -$ GIT_USER= yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/docsite/babel.config.js b/docsite/babel.config.js deleted file mode 100644 index e00595dae..000000000 --- a/docsite/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [require.resolve('@docusaurus/core/lib/babel/preset')], -}; diff --git a/docsite/docs/api/api-secrets.mdx b/docsite/docs/api/api-secrets.mdx deleted file mode 100644 index 19ac63a00..000000000 --- a/docsite/docs/api/api-secrets.mdx +++ /dev/null @@ -1,42 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# api secrets - -these routes are used to manage api secrets. - -| name | route | -| ---- | ------ | -| [create api secret](/api/api-secrets#create-api-secret) | `POST /api/secret/create` | -| [delete api secret](/api/api-secrets#delete-api-secret) | `DELETE /api/secret/delete/` | - -```mdx-code-block - -``` - -## create api secret -`POST /api/secret/create` - -### request body -```json -{ - name: string, // name the secret. must be unique among the users secrets - expires?: rfc3339_timestamp, // optional expiry time. if none, the secret will not expire. -} -``` - -### response body -```json -string // the body will be the secret hash used to log in. -``` - -```mdx-code-block - -``` - -## delete api secret -`DELETE /api/secret/delete/` - -### response -```json -HTTP 200 OK -``` \ No newline at end of file diff --git a/docsite/docs/api/authenticating-requests.md b/docsite/docs/api/authenticating-requests.md deleted file mode 100644 index d4f444cf1..000000000 --- a/docsite/docs/api/authenticating-requests.md +++ /dev/null @@ -1,8 +0,0 @@ -# authenticating requests - -monitor uses the `JSON Web Token (JWT)` standard to authenticate all requests to subroutes under `/api`. -users can acquire a `JWT` using a [login method](/api/login). - -to authenticate requests, pass the `JWT` under the `Authorization` header: - -`Authorization: Bearer ` \ No newline at end of file diff --git a/docsite/docs/api/build.mdx b/docsite/docs/api/build.mdx deleted file mode 100644 index 6fa6f2dc6..000000000 --- a/docsite/docs/api/build.mdx +++ /dev/null @@ -1,224 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# build - -these routes relate to interacting with monitor `builds` - -| name | route | -| ---- | ------ | -| [list builds](/api/build#list-builds) | `GET /api/build/list` | -| [get build](/api/build#get-build) | `GET /api/build/` | -| [get build action state](/api/build#get-build-action-state) | `GET /api/build//action_state` | -| [get build versions](/api/build#get-build-versions) | `GET /api/build//versions` | -| [create build](/api/build#create-build) | `POST /api/build/create` | -| [create full build](/api/build#create-full-build) | `POST /api/build/create_full` | -| [copy build](/api/build#copy-build) | `POST /api/build//copy` | -| [delete build](/api/build#delete-build) | `DELETE /api/build//delete` | -| [update build](/api/build#update-build) | `PATCH /api/build/update` | -| [build](/api/build#build-action) | `POST /api/build//build` | -| [get aws builder defaults](/api/build#get-aws-builder-defaults) | `GET /api/build/aws_builder_defaults` | -| [get allowed docker organizations](/api/build#get-allowed-docker-organizations) | `GET /api/build/docker_organizations` | - -```mdx-code-block - -``` - -## list builds -`GET /api/build/list` - -this method will return an array of builds the requesting user has a minimum of `Read` permissions on. - - -### response body -Array<[Build](/api/types#build)> - -```mdx-code-block - -``` - -## get build -`GET /api/build/` - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## get build action state -`GET /api/build//action_state` - -this method returns the action state for the build, eg. whether the build is currently `building`. - - -### response body -```json -{ - building: boolean, - updating: boolean, -} -``` - -```mdx-code-block - -``` - -## get build versions -`GET /api/build//versions` - -paginated route for fetching the most recent available versions of this build. - -### query params -```json -page=number // optional, default is 0. pagination starting at page 0. -major=number // optional. filter by major version number -minor=number // optional. filter by minor version number -patch=number // optional. filter by patch version number -``` - -### response body -```json -[ - { - ts: rfc3339_timestamp, - version: { - major: number, - minor: number, - patch: number, - } - }, - ... -] -``` - -```mdx-code-block - -``` - -## create build -`POST /api/build/create` - - -### request body -```json -{ - name: string, -} -``` - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## create full build -`POST /api/build/create_full` - - -### request body -[Build](/api/types#build) - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## copy build -`POST /api/build//copy` - -this method will create a copy of the build with a new _id and name, -with all the same configuration as the target build. - - -### request body -```json -{ - name: string, // the new name -} -``` - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## delete build -`DELETE /api/build//delete` - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## update build -`PATCH /api/build/update` - -### request body -[Build](/api/types#build) - -### response body -[Build](/api/types#build) - -```mdx-code-block - -``` - -## build (action) -`POST /api/build//build` - -### response body -[Update](/api/types#update) - -:::note -this update will include the `version` field. -::: - -```mdx-code-block - -``` - -## get aws builder defaults -`GET /api/build/aws_builder_defaults` - -### response body -```json -{ - default_ami_name: string, - default_subnet_id: string, - default_key_pair_name: string, - default_region: string, - default_volume_gb: number, - default_instance_type: string, - default_security_group_ids: string[], - default_assign_public_ip: boolean, - available_ami_accounts: [ - { - ami_id: string, - github: string[], - docker: string[], - secrets: string[], - } - ], -} -``` - -```mdx-code-block - -``` - -## get allowed docker organizations -`GET /api/build/docker_organizations` - -### response body -```json -string[] // the names of the allowed docker organizations -``` \ No newline at end of file diff --git a/docsite/docs/api/deployment.mdx b/docsite/docs/api/deployment.mdx deleted file mode 100644 index 5018c5539..000000000 --- a/docsite/docs/api/deployment.mdx +++ /dev/null @@ -1,344 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# deployment - -these routes relate to interacting with monitor `deployments` - -| name | route | -| ---- | ------ | -| [list deployments](/api/deployment#list-deployments) | `GET /api/deployment/list` | -| [get deployment](/api/deployment#get-deployment) | `GET /api/deployment/` | -| [get deployment action state](/api/deployment#get-deployment-action-state) | `GET /api/deployment//action_state` | -| [get deployment container log](/api/deployment#get-deployment-container-log) | `GET /api/deployment//log` | -| [get deployment container stats](/api/deployment#get-deployment-container-stats) | `GET /api/deployment//stats` | -| [get deployment deployed version](/api/deployment#get-deployment-deployed-version) | `GET /api/deployment//deployed_version` | -| [create deployment](/api/deployment#create-deployment) | `POST /api/deployment/create` | -| [create full deployment](/api/deployment#create-full-deployment) | `POST /api/deployment/create_full` | -| [copy deployment](/api/deployment#copy-deployment) | `POST /api/deployment//copy` | -| [delete deployment](/api/deployment#delete-deployment) | `DELETE /api/deployment//delete` | -| [update deployment](/api/deployment#update-deployment) | `PATCH /api/deployment/update` | -| [rename deployment](/api/deployment#rename-deployment) | `PATCH /api/deployment//rename` | -| [reclone deployment](/api/deployment#reclone-deployment) | `POST /api/deployment//reclone` | -| [pull deployment](/api/deployment#pull-deployment) | `POST /api/deployment//pull` | -| [deploy container](/api/deployment#deploy-container) | `POST /api/deployment//deploy` | -| [start container](/api/deployment#start-container) | `POST /api/deployment//start_container` | -| [stop container](/api/deployment#stop-container) | `POST /api/deployment//stop_container` | -| [remove container](/api/deployment#remove-container) | `POST /api/deployment//remove_container` | - -```mdx-code-block - -``` - -## list deployments -`GET /api/deployment/list` - -this method will return an array of deployments with container state that the requesting user has a minimum of `Read` permissions on. - -### response body -```json -[ - { - deployment: Deployment, - state: DockerContainerState, - container?: { - name: string, - id: string, - image: string, - state: DockerContainerState, - status?: string, - } - }, - ... -] -``` - -```mdx-code-block - -``` - -## get deployment -`GET /api/deployment/` - -this method will return the deployment with container state that -the requesting user has a minimum of `Read` permissions on. -it will return `500: Internal Server Error` if the user does not have the required permissions. - -### response body -```json -{ - deployment: Deployment, - state: DockerContainerState, - container?: { - name: string, - id: string, - image: string, - state: DockerContainerState, - status?: string, - } -} -``` - -```mdx-code-block - -``` -## get deployment action state -`GET /api/deployment//action_state` - -this method returns the action state for the deployment, eg. whether the deployment is currently `deploying`. - -### response body -```json -{ - deploying: boolean, - stopping: boolean, - starting: boolean, - removing: boolean, - pulling: boolean, - recloning: boolean, - updating: boolean, - renaming: boolean, -} -``` - -```mdx-code-block - -``` - -## get deployment container log -`GET /api/deployment//log` - -this method is used to get the container's log associated with the deployment. - -### query params -```json -{ - tail: number // number of log lines to fetch. this is passed to the --tail flag of docker logs command -} -``` - -### response body -```json -{ - stdout: string, - stderr: string, -} -``` - -```mdx-code-block - -``` - -## get deployment container stats -`GET /api/deployment//stats` - -this method returns the results of running `docker stats ` -for the container associated with the deployment. - -### response body -```json -{ - name: string, - cpu_perc: string, - mem_perc: string, - mem_usage: string, - net_io: string, - block_io: string, - pids: string, -} -``` - -```mdx-code-block - -``` - -## get deployment deployed version -`GET /api/deployment//deployed_version` - -this method is used to get the image version of the container associated with the deployment, if it exists. -otherwise, it will return the version specified in the deployment config. - -### response body -```json -string // the deployed version like '0.2.4' -``` - -```mdx-code-block - -``` - -## create deployment -`POST /api/deployment/create` - -this method is used to create a new deployment on a particular server. -it will return the created deployment. - -:::note -users must be **admin** or have `update` permissions on the server specified by the `server_id` -in the request body in order for this request to succeed. -::: - -### request body -```json -{ - name: string, - server_id: string, -} -``` - -### response body -[Deployment](/api/types#deployment) - -```mdx-code-block - -``` - -## create full deployment -`POST /api/deployment/create_full` - -this method is used to create a new deployment on a particular server, already initialized with config. -it will return the created deployment - -### request body -[Deployment](/api/types#deployment) - -### response body -[Deployment](/api/types#deployment) - -```mdx-code-block - -``` - -## copy deployment -`POST /api/deployment//copy` - -this method will create a copy of the deployment with a new _id and name, -with all the same configuration as the target deployment. -it can be used to move the deployment to another server. - -### request body -```json -{ - name: string, - server_id: string, -} -``` - -### response body -[Deployment](/api/types#deployment) - -```mdx-code-block - -``` - -## delete deployment -`DELETE /api/deployment//delete` - -this method will delete the deployment. if a container is associated with the deployment, it will be destroyed. - -### response body -[Deployment](/api/types#deployment) - -```mdx-code-block - -``` - -## update deployment -`PATCH /api/deployment/update` - -### request body -[Deployment](/api/types#deployment) - -### response body -[Deployment](/api/types#deployment) - -```mdx-code-block - -``` - -## rename deployment -`PATCH /api/deployment//rename` - -### request body -```json -{ - new_name: string, -} -``` - -```mdx-code-block - -``` - -## reclone deployment -`POST /api/deployment//reclone` - -if the deployment has a repo attached, this will reclone the repo, -including the on-clone and on-pull actions. - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## pull deployment -`POST /api/deployment//pull` - -if the deployment has a repo attached, this will `git pull` in the repo, -including the on-pull action. - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## deploy container -`POST /api/deployment//deploy` - -this will deploy the container corresponding to the deployments configuration. -if the container already exists, it will destroy it first. - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## start container -`POST /api/deployment//start_container` - -this will run `docker start ` for the container -corresponding to the deployment - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## stop container -`POST /api/deployment//stop_container` - -this will run `docker stop ` for the container -corresponding to the deployment - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## remove container -`POST /api/deployment//remove_container` - -this will run `docker stop && docker container rm ` -for the container corresponding to the deployment - -### response body -[Update](/api/types#update) diff --git a/docsite/docs/api/index.mdx b/docsite/docs/api/index.mdx deleted file mode 100644 index 3922e708d..000000000 --- a/docsite/docs/api/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -slug: /api ---- - -this section documents the rest and websocket api - -```mdx-code-block -import DocCardList from '@theme/DocCardList'; - - -``` \ No newline at end of file diff --git a/docsite/docs/api/login.mdx b/docsite/docs/api/login.mdx deleted file mode 100644 index 83f6c6031..000000000 --- a/docsite/docs/api/login.mdx +++ /dev/null @@ -1,103 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# login - -monitor supports local login (username and password), Oauth2 login (github and google), -and secret login (username and API secret key). -each method must be explicitly enabled in your monitor core config, -otherwise the api won't be available. - -:::note -in order to login to an Oauth2 user's account programmatically, -you must [create an api secret](/api/api-secrets#create-api-secret) and login using [/auth/secret/login](/api/login#login-using-api-secret) -::: - -| name | route | -| ---- | ------ | -| [get login options](/api/login#get-login-options) | `GET /auth/options` | -| [create local user account](/api/login#create-local-user-account) | `POST /auth/local/create_user` | -| [login local user account](/api/login#login-local-user-account) | `POST /auth/local/login` | -| [login using api secret](/api/login#login-using-api-secret) | `POST /auth/secret/login` | - -```mdx-code-block - -``` - -## get login options -`GET /auth/options` - -this method is used to obtain the login options for monitor core - -### response body -```json -{ - local: boolean, - github: boolean, - google: boolean, -} -``` - -```mdx-code-block - -``` - -## create local user account -`POST /auth/local/create_user` - -this method will create a new local auth account with the provided **username** and **password**, -and return a `JWT` for the user to authenticate with. - -### request body -```json -{ - username: string, - password: string, -} -``` - -### response body -`` - -:::caution -a user created with this method is, by default, `disabled`. a monitor admin must enable their account before they can access the API. -::: - -```mdx-code-block - -``` - -## login local user account -`POST /auth/local/login` - -this method will authenticate a local users credentials and return a JWT if login is successful. - -### request body -```json -{ - username: string, - password: string, -} -``` - -### response body -`` - -```mdx-code-block - -``` - -## login using api secret -`POST /auth/secret/login` - -this method will authenticate a users account of any kind using an api secret generated using [/api/secret/create](/api/api-secrets#create-api-secret) - -### request body -```json -{ - username: string, - secret: string, -} -``` - -### response body -`` \ No newline at end of file diff --git a/docsite/docs/api/permissions.mdx b/docsite/docs/api/permissions.mdx deleted file mode 100644 index 2170c267d..000000000 --- a/docsite/docs/api/permissions.mdx +++ /dev/null @@ -1,90 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# permissions - -these routes relate to updating user permissions - -:::note -these routes can only be called by **admin** users -::: - -| name | route | -| ---- | ------ | -| [update user permissions on target](/api/permissions#update-user-permissions-on-target) | `POST /api/permissions/update` | -| [modify user enabled](/api/permissions#modify-user-enabled) | `POST /api/permissions/modify_enabled` | -| [modify user create server permissions](/api/permissions#modify-user-create-server-permissions) | `POST /api/permissions/modify_create_server` | -| [modify user create build permissions](/api/permissions#modify-user-create-build-permissions) | `POST /api/permissions/modify_create_build` | - -```mdx-code-block - -``` - -## update user permissions on target -`POST /api/permissions/update` - -### request body -```json -{ - user_id: string, // the target users id - permission: "none" | "read" | "execute" | "update", - target_type: "server" | "deployment" | "build" | "procedure" | "group", - target_id: string, // the target resources id -} -``` - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## modify user enabled -`POST /api/permissions/modify_enabled` - -### request body -```json -{ - user_id: string, // the target users id - enabled: boolean, -} -``` - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## modify user create server permissions -`POST /api/permissions/modify_create_server` - -### request body -```json -{ - user_id: string, // the target users id - create_server_permissions: boolean, -} -``` - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## modify user create build permissions -`POST /api/permissions/modify_create_build` - -### request body -```json -{ - user_id: string, // the target users id - create_build_permissions: boolean, -} -``` - -### response body -[Update](/api/types#update) \ No newline at end of file diff --git a/docsite/docs/api/procedure.mdx b/docsite/docs/api/procedure.mdx deleted file mode 100644 index f484bb266..000000000 --- a/docsite/docs/api/procedure.mdx +++ /dev/null @@ -1,10 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# procedure - -these routes relate to interacting with monitor `procedures` - -```mdx-code-block - -``` - diff --git a/docsite/docs/api/server.mdx b/docsite/docs/api/server.mdx deleted file mode 100644 index 8ded8bfb6..000000000 --- a/docsite/docs/api/server.mdx +++ /dev/null @@ -1,473 +0,0 @@ -import Divider from '@site/src/components/Divider'; - -# server - -these routes relate to interacting with monitor `servers` - -| name | route | -| ---- | ------ | -| [list servers](/api/server#list-servers) | `GET /api/server/list` | -| [get server](/api/server#get-server) | `GET /api/server/` | -| [get server action state](/api/server#get-server-action-state) | `GET /api/server//action_state` | -| [get server github accounts](/api/server#get-server-github-accounts) | `GET /api/server//github_accounts` | -| [get server docker accounts](/api/server#get-server-docker-accounts) | `GET /api/server//docker_accounts` | -| [get server available secrets](/api/server#get-server-available-secrets) | `GET /api/server//secrets` | -| [create server](/api/server#create-server) | `POST /api/server/create` | -| [create full server](/api/server#create-full-server) | `POST /api/server/create_full` | -| [delete server](/api/server#delete-server) | `DELETE /api/server//delete` | -| [update server](/api/server#update-server) | `PATCH /api/server/update` | -| [get server periphery version](/api/server#get-server-periphery-version) | `GET /api/server//version` | -| [get server system information](/api/server#get-server-system-information) | `GET /api/server//system_information` | -| [get server stats](/api/server#get-server-stats) | `GET /api/server//stats` | -| [get server stats history](/api/server#get-server-stats-history) | `GET /api/server//stats/history` | -| [get server stats at time](/api/server#get-server-stats-at-time) | `GET /api/server//stats/at_ts` | -| [get docker networks](/api/server#get-docker-networks) | `GET /api/server//networks` | -| [prune docker networks](/api/server#prune-docker-networks) | `POST /api/server//networks/prune` | -| [get docker images](/api/server#get-docker-images) | `GET /api/server//images` | -| [prune docker images](/api/server#prune-docker-images) | `POST /api/server//images/prune` | -| [get docker containers](/api/server#get-docker-containers) | `GET /api/server//containers` | -| [prune docker containers](/api/server#prune-docker-containers) | `POST /api/server//containers/prune` | - - -```mdx-code-block - -``` - -## list servers -`GET /api/server/list` - -this method will return an array of servers with their status -that the requesting user has a minimum of `Read` permissions on. - -### response body -```json -[ - { - server: Server, - status: ServerStatus - }, - ... -] -``` - -```mdx-code-block - -``` - -## get server -`GET /api/server/` - -this method will return the server with server status that -the requesting user has a minimum of `Read` permissions on. -it will return `500: Internal Server Error` if the user does not have the required permissions. - -### response body -```json -{ - server: Server, - status: ServerStatus -} -``` - -```mdx-code-block - -``` - -## get server action state -`GET /api/server//action_state` - -this method returns the action state for the server, eg. whether the server is currently `pruning_images`. - - -### response body -```json -{ - pruning_networks: boolean, - pruning_containers: boolean, - pruning_images: boolean, -} -``` - -```mdx-code-block - -``` - -## get server github accounts -`GET /api/server//github_accounts` - -this method returns a list of all the github account usernames that are available on the server, -as defined in the server's periphery config under [github_accounts]. - -### response body -```json -["", "", ...] -``` - -```mdx-code-block - -``` - -## get server docker accounts -`GET /api/server//docker_accounts` - -this method returns a list of all the docker account usernames that are available on the server, -as defined in the server's periphery config under [docker_accounts]. - -### response body -```json -["", "", ...] -``` - -```mdx-code-block - -``` - -## get server available secrets -`GET /api/server//secrets` - -this method returns a list of all the secret keys that are available on the server, -as defined in the server's periphery config under [secrets]. - -### response body -```json -["", "", ...] -``` - -```mdx-code-block - -``` - -## create server -`POST /api/server/create` - -### request body -```json -{ - name: string, - address: string, // eg. http://12.34.56.78:8000 -} -``` - -### response body -[Server](/api/types#server) - -```mdx-code-block - -``` - -## create full server -`POST /api/server/create_full` - -this method is used to create a new server, already initialized with config. -it will return the created server. - -### request body -[Server](/api/types#server) - -### response body -[Server](/api/types#server) - -```mdx-code-block - -``` - -## delete server -`DELETE /api/server//delete` - -this method will delete the server, along with all deployments attached to the server. - -### response body -[Server](/api/types#server) - -```mdx-code-block - -``` - -## update server -`PATCH /api/server/update` - -this method is used to update a servers configuration. - -### request body -[Server](/api/types#server) - -### response body -[Server](/api/types#server) - -```mdx-code-block - -``` - -## get server periphery version -`GET /api/server//version` - -this method is used to get the version of the periphery binary running on the server. - -### response body -```json -string // the periphery version -``` - -```mdx-code-block - -``` - -## get server system information -`GET /api/server//system_information` - -this method gets some information about the host system running the periphery binary. - -### response body -```json -{ - name?: string, // the name of the system - os?: string, // the os the system is running - kernel?: string, // the version of the kernel - core_count?: number, // number of cores in the cpu - host_name?: string, // host name of the system - cpu_brand: string, // information on the cpu of the system -} -``` - -```mdx-code-block - -``` - -## get server stats -`GET /api/server//stats` - -this method retrieves current system stats of the server. - -### query params -```json -cpus=boolean // optional. if true, response will include information about each core individually -disks=boolean // optional. if true, response will include breakdown of disk usage by mount point -networks=boolean // optional. if true, response will include info on network usage -components=boolean // optional. if true, response will include component tempurature -processes=boolean // optional. if true, response will include all system processes running on host and their resource usage -``` - -### response body -```json -{ - system_load: number, - cpu_perc: number, - cpu_freq_mhz: number, - mem_used_gb: number, - mem_total_gb: number, - disk: {}, - cpus: [], - networks: [], - components: [], - processes: [], - polling_rate: Timelength, - refresh_ts: number, - refresh_list_ts: number, -} -``` - -```mdx-code-block - -``` - -## get server stats history -`GET /api/server//stats/history` - -this method will return historical system stats for the server. -the response is paginated, to get older data, specify a higher page number. - -### query params -```json -interval=Timelength // optional, default interval is 1-hr. controls granularity of historical data -limit=number // optional, default is 100, max is 500. specifies the number of data points to fetch -page=number // optional, default is 0. specifies the page of data, going backward in time. -networks=boolean // optional. if true, response will include historical info on network usage -components=boolean // optional. if true, response will include historical component tempuratures -``` - -### response body -```json -[ - { - ts: number, // unix timestamp in ms - server_id: string // specifies the server - system_load: number, - cpu_perc: number, - cpu_freq_mhz: number, - mem_used_gb: number, - mem_total_gb: number, - disk: {}, - cpus: [], - networks: [], - components: [], - processes: [], - polling_rate: Timelength, - }, - ... -] -``` - -```mdx-code-block - -``` - -## get server stats at time -`GET /api/server//stats/at_ts` - -this method retrieves the historical stats for a server at a specific timestamp - -### query params -```json -ts=number // required. the timestamp in ms -``` - -### response body -```json -{ - ts: number, // unix timestamp in ms - server_id: string // specifies the server - system_load: number, - cpu_perc: number, - cpu_freq_mhz: number, - mem_used_gb: number, - mem_total_gb: number, - disk: {}, - cpus: [], - networks: [], - components: [], - processes: [], - polling_rate: Timelength, -} -``` - -```mdx-code-block - -``` - -## get docker networks -`GET /api/server//networks` - -this method retrieves the docker networks on the server - -### response body -```json -[ - { - Name?: string, - Id?: string, - Created?: string, - Scope?: string, - Driver?: string, - EnableIPv6?: boolean, - IPAM?: { - Driver?: string, - Config?: [ - { - Subnet?: string, - IPRange?: string, - Gateway?: string, - AuxiliaryAddresses?: {} - }, - ... - ], - Options?: {} - }, - Internal?: boolean, - Attachable?: boolean, - Ingress?: boolean, - Containers?: {}, - Options?: {}, - Labels?: {} - }, - ... -] -``` - -```mdx-code-block - -``` - -## prune docker networks -`POST /api/server//networks/prune` - -this method triggers the `network prune` action on the server, which runs -`docker network prune -f` on the target server - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## get docker images -`GET /api/server//images` - -this method will return a list of images available locally on the server - -### response body -```json -[ - { - Id: string, - ParentId: string, - RepoTags: [string], - RepoDigests: [string], - Created: number, - Size: number, - SharedSize: number, - VirtualSize: number, - Labels: {}, - Containers: number, - } -] -``` - -```mdx-code-block - -``` - -## prune docker images -`POST /api/server//images/prune` - -this method triggers the `image prune` action, which runs -`docker image prune -a -f` on the target server - -### response body -[Update](/api/types#update) - -```mdx-code-block - -``` - -## get docker containers -`GET /api/server//containers` - -this method is used to retrieve information about all the containers on the target server - -### response body -```json -[ - { - name: string, - id: string, - image: string, - state: DockerContainerState, - status?: string, - }, - ... -] -``` - -```mdx-code-block - -``` - -## prune docker containers -`POST /api/server//containers/prune` - -this method triggers the `container prune` action, which runs -`docker container prune -f` on the target server - -### response body -[Update](/api/types#update) diff --git a/docsite/docs/api/types.mdx b/docsite/docs/api/types.mdx deleted file mode 100644 index 1d5aaf605..000000000 --- a/docsite/docs/api/types.mdx +++ /dev/null @@ -1,283 +0,0 @@ -import Divider from "@site/src/components/Divider"; - -# types - -these types are used across the monitor api, defined using `typescript`. they are referenced throughout the api docs. - -```mdx-code-block - -``` - -## build - -```typescript -interface Build { - _id?: { - $oid: string; - }; - name: string; - description?: string; - permissions?: { - [user_id: string]: PermissionLevel; - }; - skip_secret_interp?: boolean; - server_id?: string; - aws_config?: { - region?: string; - instance_type?: string; - ami_name?: string; - volume_gb?: number; - subnet_id?: string; - security_group_ids?: string[]; - key_pair_name?: string; - assign_public_ip?: boolean; - }; - version: { - major: number; - minor: number; - patch: number; - }; - repo?: string; - branch?: string; - github_account?: string; - pre_build?: { - path?: string; - command?: string; - }; - docker_build_args?: { - build_path: string; - dockerfile_path?: string; - build_args?: Array<{ - variable: string; - value: string; - }>; - extra_args?: string[]; - use_buildx?: boolean; - }; - docker_account?: string; - docker_organization?: string; - last_built_at?: string; - created_at?: string; - updated_at?: string; -} -``` - -```mdx-code-block - -``` - -## deployment - -```typescript -interface Deployment { - _id?: { - $oid: string; - }; - name: string; - description?: string; - server_id: string; - permissions?: PermissionLevel; - skip_secret_interp?: boolean; - docker_run_args: { - image: string; - ports?: Array<{ - local: string; - container: string; - }>; - volumes?: Array<{ - local: string; - container: string; - }>; - environment?: Array<{ - variable: string; - value: string; - }>; - network?: string; - restart?: "no" | "on-failure" | "always" | "unless-stopped"; - post_image?: string; - container_user?: string; - extra_args?: string[]; - docker_account?: string; - }; - build_id?: string; - build_version?: { - major: number; - minor: number; - patch: number; - }; - repo?: string; - branch?: string; - github_account?: string; - on_clone?: { - path?: string; - command?: string; - }; - on_pull?: { - path?: string; - command?: string; - }; - repo_mount?: { - local: string; - container: string; - }; - created_at?: string; - updated_at?: string; -} -``` - -```mdx-code-block - -``` - -## server - -```typescript -interface Server { - _id?: string; - name: string; - description?: string; - address: string; - permissions?: { - [user_id: string]: PermissionLevel; - }; - enabled: boolean; - to_notify?: string[]; - auto_prune?: boolean; - cpu_alert?: number; - mem_alert?: number; - disk_alert?: number; - stats_interval?: Timelength; - region?: string; - instance_id?: string; - created_at?: string; - updated_at?: string; -} -``` - -```mdx-code-block - -``` - -## update - -```typescript -interface Update { - _id?: string; - target: { - type: "System" | "Build" | "Deployment" | "Server" | "Procedure" | "Group"; - id?: string; - }; - operation: Operation; - logs: Array<{ - stage: string; - command: string; - stdout: string; - stderr: string; - success: boolean; - start_ts: string; - end_ts: string; - }>; - start_ts: string; - end_ts?: string; - status: "queued" | "in_progress" | "complete"; - success: boolean; - operator: string; - version?: { - major: number; - minor: number; - patch: number; - }; -} -``` - -```mdx-code-block - -``` - -## operation - -```typescript -enum Operation { - None = "none", - CreateServer = "create_server", - UpdateServer = "update_server", - DeleteServer = "delete_server", - PruneImagesServer = "prune_images_server", - PruneContainersServer = "prune_containers_server", - PruneNetworksServer = "prune_networks_server", - RenameServer = "rename_server", - CreateBuild = "create_build", - UpdateBuild = "update_build", - DeleteBuild = "delete_build", - BuildBuild = "build_build", - CreateDeployment = "create_deployment", - UpdateDeployment = "update_deployment", - DeleteDeployment = "delete_deployment", - DeployContainer = "deploy_container", - StopContainer = "stop_container", - StartContainer = "start_container", - RemoveContainer = "remove_container", - PullDeployment = "pull_deployment", - RecloneDeployment = "reclone_deployment", - RenameDeployment = "rename_deployment", - CreateProcedure = "create_procedure", - UpdateProcedure = "update_procedure", - DeleteProcedure = "delete_procedure", - CreateGroup = "create_group", - UpdateGroup = "update_group", - 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", -} -``` - -```mdx-code-block - -``` - -## permission level - -```typescript -enum PermissionLevel { - None = "none", - Read = "read", - Execute = "execute", - Update = "update", -} -``` - -```mdx-code-block - -``` - -## timelength - -```typescript -enum Timelength { - OneSecond = "1-sec", - FiveSeconds = "5-sec", - TenSeconds = "10-sec", - FifteenSeconds = "15-sec", - ThirtySeconds = "30-sec", - OneMinute = "1-min", - TwoMinutes = "2-min", - FiveMinutes = "5-min", - TenMinutes = "10-min", - FifteenMinutes = "15-min", - ThirtyMinutes = "30-min", - OneHour = "1-hr", - TwoHours = "2-hr", - SixHours = "6-hr", - EightHours = "8-hr", - TwelveHours = "12-hr", - OneDay = "1-day", - ThreeDay = "3-day", - OneWeek = "1-wk", - TwoWeeks = "2-wk", - ThirtyDays = "30-day", -} -``` diff --git a/docsite/docs/build-images/choosing-builder.md b/docsite/docs/build-images/choosing-builder.md deleted file mode 100644 index 8665b9128..000000000 --- a/docsite/docs/build-images/choosing-builder.md +++ /dev/null @@ -1,9 +0,0 @@ -# select a builder - -A builder is a machine running monitor periphery and docker. Any server connected to monitor can be chosen as the builder for a build. - -Building on a machine running production software is usually not a great idea, as this process can use a lot of system resources. It is better to start up a temporary cloud machine dedicated for the build, then shut it down when the build is finished. Right now monitor supports AWS ec2 for this task. - -### AWS builder - -You can choose to build on AWS on the "builder" tab on the build's page. From here you can select preconfigured AMIs to use as a base to build the image. These must be configured in the monitor core configuration along with other information like defaults to use, AWS credentials, etc. This is explained on the [core setup page](https://github.com/mbecker20/monitor/blob/main/docs/setup.md). \ No newline at end of file diff --git a/docsite/docs/build-images/configuration.md b/docsite/docs/build-images/configuration.md deleted file mode 100644 index 60b361ad9..000000000 --- a/docsite/docs/build-images/configuration.md +++ /dev/null @@ -1,29 +0,0 @@ -# configuration - -monitor just needs a bit of information in order to build your image. - -### repo configuration -To specify the github repo to build, just give it the name of the repo and the branch under *repo 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. - -### docker build configuration - -In order to docker build, monitor just needs to know the build directory and the path of the Dockerfile relative to the repo, you can configure these in the *build config* section. - -If the build directory is the root of the repository, you pass the build path as ```.```. If the build directory is some folder of the repo, just pass the name of the the folder. Do not pass the preceding "/". for example ```build/directory``` - -The dockerfile's path is given relative to the build directory. So if your build directory is ```build/directory``` and the dockerfile is in ```build/directory/Dockerfile.example```, you give the dockerfile path simply as ```Dockerfile.example```. - -Just as with private repos, you will need to select a docker account to use with ```docker push```. - -### adding build args - -The Dockerfile may make use of [build args](https://docs.docker.com/engine/reference/builder/#arg). Build args can be passed using the gui by pressing the ```edit``` button. They are passed in the menu just like in the would in a .env file: - -``` -BUILD_ARG1=some_value -BUILD_ARG2=some_other_value -``` - -Note that these values are visible in the final image using ```docker history```, so shouldn't be used to pass build time secrets. Use [secret mounts](https://docs.docker.com/engine/reference/builder/#run---mounttypesecret) for this instead. \ No newline at end of file diff --git a/docsite/docs/build-images/index.mdx b/docsite/docs/build-images/index.mdx deleted file mode 100644 index 91a18f23d..000000000 --- a/docsite/docs/build-images/index.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -slug: /build-images ---- - -# building images - -Monitor builds docker images by cloning the source repository from Github, running ```docker build```, and pushing the resulting image to docker hub. Any repo containing a ```Dockerfile``` is buildable using this method. - -Build configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](/file-paths). - -```mdx-code-block -import DocCardList from '@theme/DocCardList'; - - -``` \ No newline at end of file diff --git a/docsite/docs/build-images/pre-build.md b/docsite/docs/build-images/pre-build.md deleted file mode 100644 index 7915faec1..000000000 --- a/docsite/docs/build-images/pre-build.md +++ /dev/null @@ -1,7 +0,0 @@ -# pre build command - -Sometimes a command needs to be run before running ```docker build```, you can configure this in the *pre build* section. - -There are two fields to pass for *pre build*. the first is *path*, which changes the 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 the command would be ```sh scripts/on-clone.sh```. Either way works fine. \ No newline at end of file diff --git a/docsite/docs/build-images/versioning.md b/docsite/docs/build-images/versioning.md deleted file mode 100644 index 8658a0778..000000000 --- a/docsite/docs/build-images/versioning.md +++ /dev/null @@ -1,3 +0,0 @@ -# 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. \ No newline at end of file diff --git a/docsite/docs/connecting-servers/add-server.md b/docsite/docs/connecting-servers/add-server.md deleted file mode 100644 index 81c12e9a5..000000000 --- a/docsite/docs/connecting-servers/add-server.md +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deployments to be hosted on a different server. Just update the address to point to the new server. \ No newline at end of file diff --git a/docsite/docs/connecting-servers/index.mdx b/docsite/docs/connecting-servers/index.mdx deleted file mode 100644 index a381e8172..000000000 --- a/docsite/docs/connecting-servers/index.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -slug: /connecting-servers ---- - -# 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 - -```mdx-code-block -import DocCardList from '@theme/DocCardList'; - - -``` diff --git a/docsite/docs/connecting-servers/setup-periphery.md b/docsite/docs/connecting-servers/setup-periphery.md deleted file mode 100644 index 7b958634a..000000000 --- a/docsite/docs/connecting-servers/setup-periphery.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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). - - 2. Create and edit your config files, following the [config example](https://github.com/mbecker20/monitor/blob/main/config_example/periphery.config.example.toml). The monitor cli can be used to add the boilerplate: ```monitor periphery gen-config --path /path/to/config.toml```. The files can be anywhere, and can be passed to periphery via the ```--config-path``` argument. - - 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. - -### example periphery start command - -``` -periphery \ - --config-path /path/to/periphery.config.base.toml \ - --config-path /other_path/to/overide-periphery-config-directory \ - --config-keyword periphery \ - --config-keyword config \ - --merge-nested-config \ - --home_dir /home/username -``` - -### passing config files - -Either file paths or directory paths can be passed to ```--config-path```. - -When using directories, the file entries can be filtered by name with the ```--config-keyword``` argument, which can be passed multiple times to add more keywords. If passed, then only config files with file names that contain all keywords will be merged. - -When passing multiple config files, later --config-path given in the command will always overide previous ones. Directory config files are merged in alphabetical order by name, so ```config_b.toml``` will overide ```config_a.toml```. - -There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the overide config. So if you pass ```allowed_ips = []``` in your overide config, the final allowed_ips will be an empty list as well. - -```--merge-nested-config``` will merge config fields recursively and extend config array fields. - -For example, with ```--merge-nested-config``` you can specify an allowed ip in the base config, and another in the overide config, they will both be present in the final config. - -Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the overide config. \ No newline at end of file diff --git a/docsite/docs/core-setup.md b/docsite/docs/core-setup.md deleted file mode 100644 index f1c368f3c..000000000 --- a/docsite/docs/core-setup.md +++ /dev/null @@ -1,40 +0,0 @@ -# core setup - -setting up monitor core is fairly simple. there are some requirements to run monitor core: - - - a valid configuration file - - an instance of MongoDB to which monitor core can connect - - docker must be installed on the host - -## 1. create the configuration file - -create a configuration file on the system, for example at `~/.monitor/core.config.toml`, and copy the [example config](https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml). fill in all the necessary information before continuing. - -:::note -to enable OAuth2 login, you must create a client on the respective OAuth provider, -for example [google](https://developers.google.com/identity/protocols/oauth2) -or [github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps). -monitor uses the `web application` login flow. -the redirect uri is `/auth/google/callback` for google and `/auth/github/callback` for github. -::: - -## 2. start monitor core - -monitor core is distributed via dockerhub under the public repo [mbecker2020/monitor_core](https://hub.docker.com/r/mbecker2020/monitor_core). - -```sh -docker run -d --name monitor-core \ - -v $HOME/.monitor/core.config.toml:/config/config.toml \ - -p 9000:9000 \ - mbecker2020/monitor_core -``` - -## first login - -monitor core should now be accessible on the specified port, so navigating to `http://
:` will display the login page. - -the first user to log in will be auto enabled and made admin. any additional users to create accounts will be disabled by default. - -## https - -monitor core itself only supports http, so a reverse proxy like [caddy](https://caddyserver.com/) should be used for https \ No newline at end of file diff --git a/docsite/docs/deploy-containers/configuration.md b/docsite/docs/deploy-containers/configuration.md deleted file mode 100644 index 2f2cecbbc..000000000 --- a/docsite/docs/deploy-containers/configuration.md +++ /dev/null @@ -1,83 +0,0 @@ -# configuration - -## choose the docker image - -There are two options to configure the docker image to deploy. - -### attaching a monitor build -If the software you want to deploy is built by monitor, you can attach the build directly to the deployment. - -By default, monitor will deploy the latest available version of the build, or you can specify a specific version using the version dropdown. - -Also by default, monitor will use the same docker account that is attached to the build in order to pull the image on the periphery server. If that account is not available on the server, you can specify another available account to use instead, this account just needs to have read access to the docker repository. - -### using a custom image -You can also manually specify an image name, like ```mongo``` or ```mbecker2020/random_image:0.1.1```. - -If the image repository is private, you can select an available docker account to use to pull the image. - -## configuring the network - -One feature of docker is that it allows for the creation of [virtual networks between containers](https://docs.docker.com/network/). Monitor allows you to specify a docker virtual network to connect the container to, or to use the host system networking to bypass the docker virtual network. - -The default selection is ```host```, which bypasses the docker virtual network layer. - -If you do select select a network other than host, you can specify port bindings with the GUI. For example, if you are running mongo (which defaults to port 27017), you could use the mapping: - -``` -27018 : 27017 -``` - -In this case, you would access mongo from outside of the container on port ```27018```. - -Note that this is not the only affect of using a network other than ```host```. For example, containers running on different networks can not communicate, and ones on the same network can not reach other containers on ```localhost``` even when they are running on the same system. This behavior can be a bit confusing if you are not familiar with it, and it can be bypassed entirely by just using ```host``` network. - -## configuring restart behavior - -Docker, like systemd, has a couple options for handling when a container exits. See [docker restart policies](https://docs.docker.com/config/containers/start-containers-automatically/). Monitor allows you to select the appropriate restart behavior from these options. - -## configuring environment variables - -Monitor enables you to easily manage environment variables passed to the container. In the GUI, click the 'edit' button on the 'environment' card, this will bring up the environment menu. - -You pass environment variables just as you would with a ```.env``` file: - -``` -ENV_VAR_1=some_value -ENV_VAR_2=some_other_value -``` - -## configuring volumes - -A docker container's filesystem is segregated from that of the host. However, it is still possible for a container to access system files and directories, this is accomplished by using [bind mounts](https://docs.docker.com/storage/bind-mounts/). - -Say your container needs to read a config file located on the system at ```/home/ubuntu/config.toml```. You can specify the bind mount to be: - -``` -/home/ubuntu/config.toml : /config/config.toml -``` - -The first path is the one on the system, the second is the path in the container. Your application would then read the file at ```/config/config.toml``` in order to load its contents. - -These can be configured easily with the GUI in the 'volumes' card. You can configure as many bind mounts as you need. - -## extra args - -Not all features of docker are mapped directly by monitor, only the most common. You can still specify any custom flags for monitor to include in the ```docker run``` command by utilizing 'extra args'. For example, you can enable log rotation using these two extra args: - -``` ---log-opt max-size=10M -``` -``` ---log-opt max-file=3 -``` - -## post image - -Sometimes you need to specify some flags to be passed directly to the application. What is put here is inserted into the docker run command after the image. For example, to pass the ```--quiet``` flag to MongoDB, the docker run command would be: - -``` -docker run -d --name mongo-db mongo:6.0.3 --quiet -``` - -In order to achieve this with monitor, just pass ```--quiet``` to 'post image'. \ No newline at end of file diff --git a/docsite/docs/deploy-containers/index.mdx b/docsite/docs/deploy-containers/index.mdx deleted file mode 100644 index da5a54a38..000000000 --- a/docsite/docs/deploy-containers/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# deploy containers - -Monitor can deploy any docker images that it can access with the configured docker accounts. It works by parsing the deployment configuration into a ```docker run``` command, which is then run on the target system. The configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well. - -Deployment configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](/file-paths). - -```mdx-code-block -import DocCardList from '@theme/DocCardList'; - - -``` diff --git a/docsite/docs/deploy-containers/lifetime-management.md b/docsite/docs/deploy-containers/lifetime-management.md deleted file mode 100644 index f686837ce..000000000 --- a/docsite/docs/deploy-containers/lifetime-management.md +++ /dev/null @@ -1,15 +0,0 @@ -# container management - -the lifetime of a docker container is more like a virtual machine. They can be created, started, stopped, and destroyed. monitor will display the state of the container and provides an API to manage all your container's lifetimes. - -this is achieved internally by running the appropriate docker command for the requested action (docker stop, docker start, etc). - -### stopping a container - -sometimes you want to stop a running application but preserve its logs and configuration, either to be restarted later or to view the logs at a later time. It is more like *pausing* the application with its current config, as no configuration (like environment variable, volume mounts, etc.) will be changed when the container is started again. - -note that in order to restart an application with updated configuration, it must be *redeployed*. stopping and starting a container again will keep all configuration as it was when the container was initially created. - -### container redeploy - -redeploying is the action of destroying a container and recreating it. If you update deployment config, these changes will not take effect until the container is redeployed. Just note this will destroy the previous containers logs along with the container itself. \ No newline at end of file diff --git a/docsite/docs/file-paths.md b/docsite/docs/file-paths.md deleted file mode 100644 index 325fcd5bb..000000000 --- a/docsite/docs/file-paths.md +++ /dev/null @@ -1,31 +0,0 @@ -# file paths - -when working with monitor, you might have to configure file or directory paths. - -## relative paths - -Where possible, it is better to use relative file paths. Using relative file paths removes the connection between the process being run and the particular server it runs on, making it easier to move things between servers. - -Where you see relative paths: - - - setting the build directory and path of the Dockerfile - - setting a pre build command path - - configuring a frontend mount (used for web apps) - -For all of the above, the path can be given relative to the root of the configured repo - -The one exception is the Dockerfile path, which is given relative to the build directory (This is done by Docker itself, and this pattern matches usage of the Docker CLI). - -There are 3 kinds of paths to pass: - - 1. to specify the root of the repo, use ```.``` as the path - 2. to specify a folder in the repo, pass it with **no** preceding ```/```. For example, ```example_folder``` or ```folder1/folder2``` - 3. to specify an absolute path on the servers filesystem, use a preceding slash, eg. ```/home/ubuntu/example```. This way should only be used if absolutely necessary, like when passing host paths when configuring docker volumes. - -### implementation - -relative file paths are joined with the path of the repo on the system using a Rust [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push). - -## Docker Volume Paths - -These are passed directly to the Docker CLI using ```--volume /path/on/system:/path/in/container```. So for these, the same rules apply as when using Docker on the command line. Paths here should be given as absolute, don't use ```~``` or even ```$HOME```. diff --git a/docsite/docs/intro.md b/docsite/docs/intro.md deleted file mode 100644 index be658694f..000000000 --- a/docsite/docs/intro.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -slug: /intro ---- - -# what is monitor? - -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). \ No newline at end of file diff --git a/docsite/docs/permissioning.md b/docsite/docs/permissioning.md deleted file mode 100644 index 4cfba20ae..000000000 --- a/docsite/docs/permissioning.md +++ /dev/null @@ -1,28 +0,0 @@ -# permissioning resources - -All monitor resources (servers, builds, deployment) have independant permission tables to allow for users to have granular access to these resources. By default, users do not see any resources until they are given at least read permissions. - -## permission levels - -There are 4 levels of permissions a user can have on a resource: - - 1. **None**. This is the lowest permission level, and means the user will not have any access to this resource. They will not see it in the GUI, and it will not show up if the user queries the core API directly. All attempts to view or update the resource will be blocked. - - 2. **Read**. This is the first permission level that grants any access. It will enable the user to see the resource in the GUI, read the configuration, and see any logs. Any attempts to update configuration or trigger any action will be blocked. - - 3. **Execute**. This level will allow the user to execute actions on the resource, like send a build command or trigger a redeploy. The user will still be blocked from updating configuration on the resource. - - 4. **Update**. The user has full access to the resource, they can execute any actions, update the configuration, and delete the resource. - -## Administration - -Users can be given admin priviledges by accessing the monitor MongoDB and setting ```admin: true``` on the intended user document. These users have unrestricted access to all monitor resources, like servers, builds, and deployments. Additionally, only these users can update other (non-admin) user's permissions on resources, an action not available to regular users even with **Update** level permissions. - -Monitor admins are responsible for managing user accounts as well. When a user logs into monitor for the first time, they will not immediately be granted access. An admin must first **enable** the user, which can be done from the 'manage users' page (found in the user dropdown menu in the topbar). Users can also be **disabled** by an admin at any time, which blocks all their access to the GUI and API. - -Users also have some configurable global permissions, these are: - - - create server permission - - create build permission - -Only users with these permissions (as well as admins) can add additional servers to monitor, and can create additional builds, respectively. \ No newline at end of file diff --git a/docsite/docusaurus.config.js b/docsite/docusaurus.config.js deleted file mode 100644 index b95e9aa80..000000000 --- a/docsite/docusaurus.config.js +++ /dev/null @@ -1,99 +0,0 @@ -// @ts-check -// Note: type annotations allow type checking and IDEs autocompletion - -const lightCodeTheme = require('prism-react-renderer/themes/github'); -const darkCodeTheme = require('prism-react-renderer/themes/dracula'); - -/** @type {import('@docusaurus/types').Config} */ -const config = { - title: "monitor", - tagline: "distributed build and deployment system", - favicon: "img/favicon.ico", - - // Set the production url of your site here - url: "https://mbecker20.github.io", - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: "/monitor/", - // baseUrl: "/", - - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: "mbecker20", // Usually your GitHub org/user name. - projectName: "monitor", // Usually your repo name. - trailingSlash: false, - deploymentBranch: "gh-pages-docs", - - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - - // Even if you don't use internalization, you can use this field to set useful - // metadata like html lang. For example, if your site is Chinese, you may want - // to replace "en" with "zh-Hans". - i18n: { - defaultLocale: "en", - locales: ["en"], - }, - - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - routeBasePath: "/", - sidebarPath: require.resolve("./sidebars.js"), - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: "https://github.com/mbecker20/monitor/tree/main/docsite", - }, - theme: { - customCss: require.resolve("./src/css/custom.css"), - }, - }), - ], - ], - - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - // Replace with your project's social card - image: "img/monitor-lizard.png", - docs: { - sidebar: { - autoCollapseCategories: true, - } - }, - navbar: { - title: "monitor", - logo: { - alt: "monitor lizard", - src: "img/monitor-lizard.png", - }, - items: [ - { - type: "docSidebar", - sidebarId: "docs", - position: "left", - label: "docs", - - }, - { - href: "https://github.com/mbecker20/monitor", - label: "GitHub", - position: "right", - }, - ], - }, - footer: { - style: "dark", - copyright: `Built with Docusaurus`, - }, - prism: { - theme: lightCodeTheme, - darkTheme: darkCodeTheme, - }, - }), -}; - -module.exports = config; diff --git a/docsite/package.json b/docsite/package.json deleted file mode 100644 index 4f25a1742..000000000 --- a/docsite/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "docsite", - "version": "0.0.0", - "private": true, - "scripts": { - "start": "docusaurus start", - "deploy": "GIT_USER=mbecker20 docusaurus deploy" - }, - "dependencies": { - "@docusaurus/core": "2.4.0", - "@docusaurus/preset-classic": "2.4.0", - "@mdx-js/react": "^1.6.22", - "clsx": "^1.2.1", - "prism-react-renderer": "^1.3.5", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "2.4.0", - "@tsconfig/docusaurus": "^1.0.5", - "typescript": "^4.7.4" - }, - "browserslist": { - "production": [ - ">0.5%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "engines": { - "node": ">=16.14" - } -} diff --git a/docsite/sidebars.js b/docsite/sidebars.js deleted file mode 100644 index dfc5755ee..000000000 --- a/docsite/sidebars.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - */ - -// @ts-check - -/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ -const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - // docsSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - docs: [ - "intro", - "core-setup", - { - type: "category", - label: "connecting servers", - link: { - type: "doc", - id: "connecting-servers/index", - }, - items: [ - "connecting-servers/setup-periphery", - "connecting-servers/add-server", - ], - }, - { - type: "category", - label: "build images", - link: { - type: "doc", - id: "build-images/index", - }, - items: [ - "build-images/configuration", - "build-images/pre-build", - "build-images/choosing-builder", - "build-images/versioning", - ], - }, - { - type: "category", - label: "deploy containers", - link: { - type: "doc", - id: "deploy-containers/index", - }, - items: [ - "deploy-containers/configuration", - "deploy-containers/lifetime-management", - // "deploy-containers/choosing-builder", - // "deploy-containers/versioning", - ], - }, - "permissioning", - "file-paths", - { - type: "category", - label: "API", - link: { - type: "doc", - id: "api/index", - }, - items: [ - "api/types", - "api/authenticating-requests", - "api/login", - "api/api-secrets", - "api/build", - "api/deployment", - "api/server", - "api/permissions", - "api/websocket", - ], - }, - ], -}; - -module.exports = sidebars; diff --git a/docsite/src/components/Divider.tsx b/docsite/src/components/Divider.tsx deleted file mode 100644 index 7c2dc38a4..000000000 --- a/docsite/src/components/Divider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -export default function Divider() { - return ( -
- ); -} diff --git a/docsite/src/components/HomepageFeatures/index.tsx b/docsite/src/components/HomepageFeatures/index.tsx deleted file mode 100644 index 683585a3f..000000000 --- a/docsite/src/components/HomepageFeatures/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; -import styles from './styles.module.css'; - -type FeatureItem = { - title: string; - // Svg: React.ComponentType>; - description: JSX.Element; -}; - -const FeatureList: FeatureItem[] = [ - { - title: 'automated builds 🛠️', - // Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, - description: ( - <> - build auto versioned docker images from github repos, trigger builds on git push - - ), - }, - { - title: 'deploy docker containers 🚀', - // Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, - description: ( - <> - deploy your builds (or any docker image), see uptime and logs across all your servers - - ), - }, - { - title: 'powered by Rust 🦀', - // Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, - description: ( - <> - The core API and periphery client are written in Rust - - ), - }, -]; - -function Feature({ title, description }: FeatureItem) { - return ( -
- {/*
- -
*/} -
-

{title}

-

{description}

-
-
- ); -} - -export default function HomepageFeatures(): JSX.Element { - return ( -
-
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
-
- ); -} diff --git a/docsite/src/components/HomepageFeatures/styles.module.css b/docsite/src/components/HomepageFeatures/styles.module.css deleted file mode 100644 index e3b130c52..000000000 --- a/docsite/src/components/HomepageFeatures/styles.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.features { - display: flex; - align-items: center; - padding: 4rem 0; - width: 100%; -} - -.featureSvg { - height: 200px; - width: 200px; -} diff --git a/docsite/src/components/MonitorLogo.tsx b/docsite/src/components/MonitorLogo.tsx deleted file mode 100644 index 5fa3e8d9c..000000000 --- a/docsite/src/components/MonitorLogo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -export default function MonitorLogo({ width = "4rem" }) { - return ( - monitor-lizard - ); -} diff --git a/docsite/src/components/SummaryImg.tsx b/docsite/src/components/SummaryImg.tsx deleted file mode 100644 index dfccf4ff8..000000000 --- a/docsite/src/components/SummaryImg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -export default function SummaryImg() { - return ( -
- monitor-summary -
- ); -} diff --git a/docsite/src/css/custom.css b/docsite/src/css/custom.css deleted file mode 100644 index 2bc6a4cfd..000000000 --- a/docsite/src/css/custom.css +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -/* You can override the default Infima variables here. */ -:root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); -} - -/* For readability concerns, you should choose a lighter palette in dark mode. */ -[data-theme='dark'] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); -} diff --git a/docsite/src/pages/index.module.css b/docsite/src/pages/index.module.css deleted file mode 100644 index 8e2bdd778..000000000 --- a/docsite/src/pages/index.module.css +++ /dev/null @@ -1,24 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: grid; - gap: 1rem; - grid-template-columns: 1fr 1fr; - width: fit-content; -} diff --git a/docsite/src/pages/index.tsx b/docsite/src/pages/index.tsx deleted file mode 100644 index d147ad070..000000000 --- a/docsite/src/pages/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import Layout from '@theme/Layout'; -import HomepageFeatures from '@site/src/components/HomepageFeatures'; - -import styles from './index.module.css'; -import SummaryImg from '../components/SummaryImg'; -import MonitorLogo from '../components/MonitorLogo'; - -function HomepageHeader() { - const {siteConfig} = useDocusaurusContext(); - return ( -
-
-
-
- -

- monitor -

-
-
-

{siteConfig.tagline}

-
-
- - docs - - - github - - - screenshots - -
-
-
-
- ); -} - -export default function Home(): JSX.Element { - const {siteConfig} = useDocusaurusContext(); - return ( - - {/* */} - -
- - {/* */} -
-
- ); -} diff --git a/docsite/static/.nojekyll b/docsite/static/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/docsite/static/img/favicon.ico b/docsite/static/img/favicon.ico deleted file mode 100644 index 82a418d29..000000000 Binary files a/docsite/static/img/favicon.ico and /dev/null differ diff --git a/docsite/static/img/logo.svg b/docsite/static/img/logo.svg deleted file mode 100644 index 9db6d0d06..000000000 --- a/docsite/static/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docsite/static/img/monitor-lizard.png b/docsite/static/img/monitor-lizard.png deleted file mode 100644 index f119d3bd0..000000000 Binary files a/docsite/static/img/monitor-lizard.png and /dev/null differ diff --git a/docsite/static/img/monitor-summary.png b/docsite/static/img/monitor-summary.png deleted file mode 100644 index b88125dfc..000000000 Binary files a/docsite/static/img/monitor-summary.png and /dev/null differ diff --git a/docsite/tsconfig.json b/docsite/tsconfig.json deleted file mode 100644 index 6f4756980..000000000 --- a/docsite/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // This file is not used in compilation. It is here just for a nice editor experience. - "extends": "@tsconfig/docusaurus/tsconfig.json", - "compilerOptions": { - "baseUrl": "." - } -} diff --git a/docsite/yarn.lock b/docsite/yarn.lock deleted file mode 100644 index 9bfce6991..000000000 --- a/docsite/yarn.lock +++ /dev/null @@ -1,7617 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz#85ff36b2673654a393c8c505345eaedd6eaa4f70" - integrity sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg== - dependencies: - "@algolia/autocomplete-shared" "1.7.4" - -"@algolia/autocomplete-preset-algolia@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz#610ee1d887962f230b987cba2fd6556478000bc3" - integrity sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ== - dependencies: - "@algolia/autocomplete-shared" "1.7.4" - -"@algolia/autocomplete-shared@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz#78aea1140a50c4d193e1f06a13b7f12c5e2cbeea" - integrity sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg== - -"@algolia/cache-browser-local-storage@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.16.0.tgz#ccd31ab9df10781a69a653e9d994d0be974952ba" - integrity sha512-jVrk0YB3tjOhD5/lhBtYCVCeLjZmVpf2kdi4puApofytf/R0scjWz0GdozlW4HhU+Prxmt/c9ge4QFjtv5OAzQ== - dependencies: - "@algolia/cache-common" "4.16.0" - -"@algolia/cache-common@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.16.0.tgz#0ef4726653d3891805c3348153067b292df1d68d" - integrity sha512-4iHjkSYQYw46pITrNQgXXhvUmcekI8INz1m+SzmqLX8jexSSy4Ky4zfGhZzhhhLHXUP3+x/PK/c0qPjxEvRwKQ== - -"@algolia/cache-in-memory@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.16.0.tgz#b9aecfc4054f2307caeea255aace65061040f680" - integrity sha512-p7RYykvA6Ip6QENxrh99nOD77otVh1sJRivcgcVpnjoZb5sIN3t33eUY1DpB9QSBizcrW+qk19rNkdnZ43a+PQ== - dependencies: - "@algolia/cache-common" "4.16.0" - -"@algolia/client-account@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.16.0.tgz#3164e28df1b6428a60f43b8a97ea669d6bbbce9e" - integrity sha512-eydcfpdIyuWoKgUSz5iZ/L0wE/Wl7958kACkvTHLDNXvK/b8Z1zypoJavh6/km1ZNQmFpeYS2jrmq0kUSFn02w== - dependencies: - "@algolia/client-common" "4.16.0" - "@algolia/client-search" "4.16.0" - "@algolia/transporter" "4.16.0" - -"@algolia/client-analytics@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.16.0.tgz#afa72be54a28fdfe00505d6ab3c179ef2c9bc796" - integrity sha512-cONWXH3BfilgdlCofUm492bJRWtpBLVW/hsUlfoFtiX1u05xoBP7qeiDwh9RR+4pSLHLodYkHAf5U4honQ55Qg== - dependencies: - "@algolia/client-common" "4.16.0" - "@algolia/client-search" "4.16.0" - "@algolia/requester-common" "4.16.0" - "@algolia/transporter" "4.16.0" - -"@algolia/client-common@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.16.0.tgz#670f4b2286755090a6389c55d79402716e4556ac" - integrity sha512-QVdR4019ukBH6f5lFr27W60trRxQF1SfS1qo0IP6gjsKhXhUVJuHxOCA6ArF87jrNkeuHEoRoDU+GlvaecNo8g== - dependencies: - "@algolia/requester-common" "4.16.0" - "@algolia/transporter" "4.16.0" - -"@algolia/client-personalization@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.16.0.tgz#927728c069b42f39fc83ce61a1fdcc79cd38b842" - integrity sha512-irtLafssDGPuhYqIwxqOxiWlVYvrsBD+EMA1P9VJtkKi3vSNBxiWeQ0f0Tn53cUNdSRNEssfoEH84JL97SV2SQ== - dependencies: - "@algolia/client-common" "4.16.0" - "@algolia/requester-common" "4.16.0" - "@algolia/transporter" "4.16.0" - -"@algolia/client-search@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.16.0.tgz#19a76bc6cfa495aa9011b32dd85305baa2579589" - integrity sha512-xsfrAE1jO/JDh1wFrRz+alVyW+aA6qnkzmbWWWZWEgVF3EaFqzIf9r1l/aDtDdBtNTNhX9H3Lg31+BRtd5izQA== - dependencies: - "@algolia/client-common" "4.16.0" - "@algolia/requester-common" "4.16.0" - "@algolia/transporter" "4.16.0" - -"@algolia/events@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" - integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== - -"@algolia/logger-common@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.16.0.tgz#8d83ebe5d798b84cf0026ae68d47b6dc8f542b61" - integrity sha512-U9H8uCzSDuePJmbnjjTX21aPDRU6x74Tdq3dJmdYu2+pISx02UeBJm4kSgc9RW5jcR5j35G9gnjHY9Q3ngWbyQ== - -"@algolia/logger-console@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.16.0.tgz#e125d63268a6cab64c625e867596a8c8201850d2" - integrity sha512-+qymusiM+lPZKrkf0tDjCQA158eEJO2IU+Nr/sJ9TFyI/xkFPjNPzw/Qbc8Iy/xcOXGlc6eMgmyjtVQqAWq6UA== - dependencies: - "@algolia/logger-common" "4.16.0" - -"@algolia/requester-browser-xhr@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.16.0.tgz#742090e53301889ae01581eedcc5bfc2c6dd7660" - integrity sha512-gK+kvs6LHl/PaOJfDuwjkopNbG1djzFLsVBklGBsSU6h6VjFkxIpo6Qq80IK14p9cplYZfhfaL12va6Q9p3KVQ== - dependencies: - "@algolia/requester-common" "4.16.0" - -"@algolia/requester-common@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.16.0.tgz#a24eb9fc9062f76c2e06599c678542e78c8bce1c" - integrity sha512-3Zmcs/iMubcm4zqZ3vZG6Zum8t+hMWxGMzo0/uY2BD8o9q5vMxIYI0c4ocdgQjkXcix189WtZNkgjSOBzSbkdw== - -"@algolia/requester-node-http@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.16.0.tgz#0723929863c201d12888c2751043b872a04bb6d9" - integrity sha512-L8JxM2VwZzh8LJ1Zb8TFS6G3icYsCKZsdWW+ahcEs1rGWmyk9SybsOe1MLnjonGBaqPWJkn9NjS7mRdjEmBtKA== - dependencies: - "@algolia/requester-common" "4.16.0" - -"@algolia/transporter@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.16.0.tgz#5e6ca73c8a72d91b7dd75b00b5e45e8538bad8b6" - integrity sha512-H9BVB2EAjT65w7XGBNf5drpsW39x2aSZ942j4boSAAJPPlLmjtj5IpAP7UAtsV8g9Beslonh0bLa1XGmE/P0BA== - dependencies: - "@algolia/cache-common" "4.16.0" - "@algolia/logger-common" "4.16.0" - "@algolia/requester-common" "4.16.0" - -"@ampproject/remapping@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" - integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" - integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== - -"@babel/core@7.12.9": - version "7.12.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" - integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helpers" "^7.12.5" - "@babel/parser" "^7.12.7" - "@babel/template" "^7.12.7" - "@babel/traverse" "^7.12.9" - "@babel/types" "^7.12.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.18.6", "@babel/core@^7.19.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" - integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.4" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.4" - "@babel/types" "^7.21.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - -"@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" - integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== - dependencies: - "@babel/types" "^7.21.4" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656" - integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== - dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" - integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz#40411a8ab134258ad2cf3a3d987ec6aa0723cee5" - integrity sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.3.1" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" - integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== - dependencies: - "@babel/types" "^7.21.0" - -"@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" - integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== - dependencies: - "@babel/types" "^7.21.4" - -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" - integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/helpers@^7.12.5", "@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.12.7", "@babel/parser@^7.18.8", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" - integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-proposal-optional-chaining" "^7.20.7" - -"@babel/plugin-proposal-async-generator-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" - integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" - integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.12.1" - -"@babel/plugin-proposal-object-rest-spread@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.20.7", "@babel/plugin-proposal-optional-chaining@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" - integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" - integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" - integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" - integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.20.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" - integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-arrow-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-async-to-generator@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoping@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" - integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-classes@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" - integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" - -"@babel/plugin-transform-destructuring@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz#73b46d0fd11cd6ef57dea8a381b1215f4959d401" - integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-for-of@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-amd@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== - dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-modules-commonjs@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== - dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" - -"@babel/plugin-transform-modules-systemjs@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-identifier" "^7.19.1" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db" - integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-constant-elements@^7.18.12": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.21.3.tgz#b32a5556100d424b25e388dd689050d78396884d" - integrity sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-react-display-name@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" - integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-jsx-development@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" - integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.18.6" - -"@babel/plugin-transform-react-jsx@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2" - integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.21.0" - -"@babel/plugin-transform-react-pure-annotations@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844" - integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-regenerator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - regenerator-transform "^0.15.1" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-runtime@^7.18.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.4.tgz#2e1da21ca597a7d01fc96b699b21d8d2023191aa" - integrity sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA== - dependencies: - "@babel/helper-module-imports" "^7.21.4" - "@babel/helper-plugin-utils" "^7.20.2" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - semver "^6.3.0" - -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-spread@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typescript@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz#316c5be579856ea890a57ebc5116c5d064658f2b" - integrity sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-typescript" "^7.20.0" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/preset-env@^7.18.6", "@babel/preset-env@^7.19.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.4.tgz#a952482e634a8dd8271a3fe5459a16eb10739c58" - integrity sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw== - dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.21.0" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7" - "@babel/plugin-proposal-async-generator-functions" "^7.20.7" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.21.0" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.20.7" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.7" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.21.0" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.21.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.20.0" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.20.7" - "@babel/plugin-transform-async-to-generator" "^7.20.7" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.21.0" - "@babel/plugin-transform-classes" "^7.21.0" - "@babel/plugin-transform-computed-properties" "^7.20.7" - "@babel/plugin-transform-destructuring" "^7.21.3" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.21.0" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.20.11" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" - "@babel/plugin-transform-modules-systemjs" "^7.20.11" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.21.3" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.20.5" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.20.7" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.21.4" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" - integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-transform-react-display-name" "^7.18.6" - "@babel/plugin-transform-react-jsx" "^7.18.6" - "@babel/plugin-transform-react-jsx-development" "^7.18.6" - "@babel/plugin-transform-react-pure-annotations" "^7.18.6" - -"@babel/preset-typescript@^7.18.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529" - integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.21.0" - "@babel/plugin-syntax-jsx" "^7.21.4" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" - "@babel/plugin-transform-typescript" "^7.21.3" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime-corejs3@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.21.0.tgz#6e4939d9d9789ff63e2dc58e88f13a3913a24eba" - integrity sha512-TDD4UJzos3JJtM+tHX+w2Uc+KWj7GV+VKKFdMVd2Rx8sdA19hcc3P3AHFYd5LVOw+pYuSd5lICC3gm52B6Rwxw== - dependencies: - core-js-pure "^3.25.1" - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.8.4": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/template@^7.12.7", "@babel/template@^7.18.10", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== - dependencies: - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.12.7", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.4.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@docsearch/css@3.3.3": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.3.3.tgz#f9346c9e24602218341f51b8ba91eb9109add434" - integrity sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg== - -"@docsearch/react@^3.1.1": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.3.3.tgz#907b6936a565f880b4c0892624b4f7a9f132d298" - integrity sha512-pLa0cxnl+G0FuIDuYlW+EBK6Rw2jwLw9B1RHIeS4N4s2VhsfJ/wzeCi3CWcs5yVfxLd5ZK50t//TMA5e79YT7Q== - dependencies: - "@algolia/autocomplete-core" "1.7.4" - "@algolia/autocomplete-preset-algolia" "1.7.4" - "@docsearch/css" "3.3.3" - algoliasearch "^4.0.0" - -"@docusaurus/core@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.4.0.tgz#a12c175cb2e5a7e4582e65876a50813f6168913d" - integrity sha512-J55/WEoIpRcLf3afO5POHPguVZosKmJEQWKBL+K7TAnfuE7i+Y0NPLlkKtnWCehagGsgTqClfQEexH/UT4kELA== - dependencies: - "@babel/core" "^7.18.6" - "@babel/generator" "^7.18.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.18.6" - "@babel/preset-env" "^7.18.6" - "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.18.6" - "@babel/runtime" "^7.18.6" - "@babel/runtime-corejs3" "^7.18.6" - "@babel/traverse" "^7.18.8" - "@docusaurus/cssnano-preset" "2.4.0" - "@docusaurus/logger" "2.4.0" - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-common" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - "@slorber/static-site-generator-webpack-plugin" "^4.0.7" - "@svgr/webpack" "^6.2.1" - autoprefixer "^10.4.7" - babel-loader "^8.2.5" - babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" - clean-css "^5.3.0" - cli-table3 "^0.6.2" - combine-promises "^1.1.0" - commander "^5.1.0" - copy-webpack-plugin "^11.0.0" - core-js "^3.23.3" - css-loader "^6.7.1" - css-minimizer-webpack-plugin "^4.0.0" - cssnano "^5.1.12" - del "^6.1.1" - detect-port "^1.3.0" - escape-html "^1.0.3" - eta "^2.0.0" - file-loader "^6.2.0" - fs-extra "^10.1.0" - html-minifier-terser "^6.1.0" - html-tags "^3.2.0" - html-webpack-plugin "^5.5.0" - import-fresh "^3.3.0" - leven "^3.1.0" - lodash "^4.17.21" - mini-css-extract-plugin "^2.6.1" - postcss "^8.4.14" - postcss-loader "^7.0.0" - prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" - react-loadable "npm:@docusaurus/react-loadable@5.5.2" - react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.3.3" - react-router-config "^5.1.1" - react-router-dom "^5.3.3" - rtl-detect "^1.0.4" - semver "^7.3.7" - serve-handler "^6.1.3" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.3" - tslib "^2.4.0" - update-notifier "^5.1.0" - url-loader "^4.1.1" - wait-on "^6.0.1" - webpack "^5.73.0" - webpack-bundle-analyzer "^4.5.0" - webpack-dev-server "^4.9.3" - webpack-merge "^5.8.0" - webpackbar "^5.0.2" - -"@docusaurus/cssnano-preset@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.0.tgz#9213586358e0cce517f614af041eb7d184f8add6" - integrity sha512-RmdiA3IpsLgZGXRzqnmTbGv43W4OD44PCo+6Q/aYjEM2V57vKCVqNzuafE94jv0z/PjHoXUrjr69SaRymBKYYw== - dependencies: - cssnano-preset-advanced "^5.3.8" - postcss "^8.4.14" - postcss-sort-media-queries "^4.2.1" - tslib "^2.4.0" - -"@docusaurus/logger@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.4.0.tgz#393d91ad9ecdb9a8f80167dd6a34d4b45219b835" - integrity sha512-T8+qR4APN+MjcC9yL2Es+xPJ2923S9hpzDmMtdsOcUGLqpCGBbU1vp3AAqDwXtVgFkq+NsEk7sHdVsfLWR/AXw== - dependencies: - chalk "^4.1.2" - tslib "^2.4.0" - -"@docusaurus/mdx-loader@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.0.tgz#c6310342904af2f203e7df86a9df623f86840f2d" - integrity sha512-GWoH4izZKOmFoC+gbI2/y8deH/xKLvzz/T5BsEexBye8EHQlwsA7FMrVa48N063bJBH4FUOiRRXxk5rq9cC36g== - dependencies: - "@babel/parser" "^7.18.8" - "@babel/traverse" "^7.18.8" - "@docusaurus/logger" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@mdx-js/mdx" "^1.6.22" - escape-html "^1.0.3" - file-loader "^6.2.0" - fs-extra "^10.1.0" - image-size "^1.0.1" - mdast-util-to-string "^2.0.0" - remark-emoji "^2.2.0" - stringify-object "^3.3.0" - tslib "^2.4.0" - unified "^9.2.2" - unist-util-visit "^2.0.3" - url-loader "^4.1.1" - webpack "^5.73.0" - -"@docusaurus/module-type-aliases@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.0.tgz#6961605d20cd46f86163ed8c2d83d438b02b4028" - integrity sha512-YEQO2D3UXs72qCn8Cr+RlycSQXVGN9iEUyuHwTuK4/uL/HFomB2FHSU0vSDM23oLd+X/KibQ3Ez6nGjQLqXcHg== - dependencies: - "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/types" "2.4.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - "@types/react-router-dom" "*" - react-helmet-async "*" - react-loadable "npm:@docusaurus/react-loadable@5.5.2" - -"@docusaurus/plugin-content-blog@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.0.tgz#50dbfbc7b51f152ae660385fd8b34076713374c3" - integrity sha512-YwkAkVUxtxoBAIj/MCb4ohN0SCtHBs4AS75jMhPpf67qf3j+U/4n33cELq7567hwyZ6fMz2GPJcVmctzlGGThQ== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/logger" "2.4.0" - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-common" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - cheerio "^1.0.0-rc.12" - feed "^4.2.2" - fs-extra "^10.1.0" - lodash "^4.17.21" - reading-time "^1.5.0" - tslib "^2.4.0" - unist-util-visit "^2.0.3" - utility-types "^3.10.0" - webpack "^5.73.0" - -"@docusaurus/plugin-content-docs@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.0.tgz#36e235adf902325735b873b4f535205884363728" - integrity sha512-ic/Z/ZN5Rk/RQo+Io6rUGpToOtNbtPloMR2JcGwC1xT2riMu6zzfSwmBi9tHJgdXH6CB5jG+0dOZZO8QS5tmDg== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/logger" "2.4.0" - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/module-type-aliases" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - "@types/react-router-config" "^5.0.6" - combine-promises "^1.1.0" - fs-extra "^10.1.0" - import-fresh "^3.3.0" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.4.0" - utility-types "^3.10.0" - webpack "^5.73.0" - -"@docusaurus/plugin-content-pages@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.0.tgz#6169909a486e1eae0ddffff0b1717ce4332db4d4" - integrity sha512-Pk2pOeOxk8MeU3mrTU0XLIgP9NZixbdcJmJ7RUFrZp1Aj42nd0RhIT14BGvXXyqb8yTQlk4DmYGAzqOfBsFyGw== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - fs-extra "^10.1.0" - tslib "^2.4.0" - webpack "^5.73.0" - -"@docusaurus/plugin-debug@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.0.tgz#1ad513fe9bcaf017deccf62df8b8843faeeb7d37" - integrity sha512-KC56DdYjYT7Txyux71vXHXGYZuP6yYtqwClvYpjKreWIHWus5Zt6VNi23rMZv3/QKhOCrN64zplUbdfQMvddBQ== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - fs-extra "^10.1.0" - react-json-view "^1.21.3" - tslib "^2.4.0" - -"@docusaurus/plugin-google-analytics@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.0.tgz#8062d7a09d366329dfd3ce4e8a619da8624b6cc3" - integrity sha512-uGUzX67DOAIglygdNrmMOvEp8qG03X20jMWadeqVQktS6nADvozpSLGx4J0xbkblhJkUzN21WiilsP9iVP+zkw== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - tslib "^2.4.0" - -"@docusaurus/plugin-google-gtag@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.0.tgz#a8efda476f971410dfb3aab1cfe1f0f7d269adc5" - integrity sha512-adj/70DANaQs2+TF/nRdMezDXFAV/O/pjAbUgmKBlyOTq5qoMe0Tk4muvQIwWUmiUQxFJe+sKlZGM771ownyOg== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - tslib "^2.4.0" - -"@docusaurus/plugin-google-tag-manager@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.0.tgz#9a94324ac496835fc34e233cc60441df4e04dfdd" - integrity sha512-E66uGcYs4l7yitmp/8kMEVQftFPwV9iC62ORh47Veqzs6ExwnhzBkJmwDnwIysHBF1vlxnzET0Fl2LfL5fRR3A== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - tslib "^2.4.0" - -"@docusaurus/plugin-sitemap@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.0.tgz#ba0eb43565039fe011bdd874b5c5d7252b19d709" - integrity sha512-pZxh+ygfnI657sN8a/FkYVIAmVv0CGk71QMKqJBOfMmDHNN1FeDeFkBjWP49ejBqpqAhjufkv5UWq3UOu2soCw== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/logger" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-common" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - fs-extra "^10.1.0" - sitemap "^7.1.1" - tslib "^2.4.0" - -"@docusaurus/preset-classic@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.4.0.tgz#92fdcfab35d8d0ffb8c38bcbf439e4e1cb0566a3" - integrity sha512-/5z5o/9bc6+P5ool2y01PbJhoGddEGsC0ej1MF6mCoazk8A+kW4feoUd68l7Bnv01rCnG3xy7kHUQP97Y0grUA== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/plugin-content-blog" "2.4.0" - "@docusaurus/plugin-content-docs" "2.4.0" - "@docusaurus/plugin-content-pages" "2.4.0" - "@docusaurus/plugin-debug" "2.4.0" - "@docusaurus/plugin-google-analytics" "2.4.0" - "@docusaurus/plugin-google-gtag" "2.4.0" - "@docusaurus/plugin-google-tag-manager" "2.4.0" - "@docusaurus/plugin-sitemap" "2.4.0" - "@docusaurus/theme-classic" "2.4.0" - "@docusaurus/theme-common" "2.4.0" - "@docusaurus/theme-search-algolia" "2.4.0" - "@docusaurus/types" "2.4.0" - -"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": - version "5.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" - integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== - dependencies: - "@types/react" "*" - prop-types "^15.6.2" - -"@docusaurus/theme-classic@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.4.0.tgz#a5404967b00adec3472efca4c3b3f6a5e2021c78" - integrity sha512-GMDX5WU6Z0OC65eQFgl3iNNEbI9IMJz9f6KnOyuMxNUR6q0qVLsKCNopFUDfFNJ55UU50o7P7o21yVhkwpfJ9w== - dependencies: - "@docusaurus/core" "2.4.0" - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/module-type-aliases" "2.4.0" - "@docusaurus/plugin-content-blog" "2.4.0" - "@docusaurus/plugin-content-docs" "2.4.0" - "@docusaurus/plugin-content-pages" "2.4.0" - "@docusaurus/theme-common" "2.4.0" - "@docusaurus/theme-translations" "2.4.0" - "@docusaurus/types" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-common" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - "@mdx-js/react" "^1.6.22" - clsx "^1.2.1" - copy-text-to-clipboard "^3.0.1" - infima "0.2.0-alpha.43" - lodash "^4.17.21" - nprogress "^0.2.0" - postcss "^8.4.14" - prism-react-renderer "^1.3.5" - prismjs "^1.28.0" - react-router-dom "^5.3.3" - rtlcss "^3.5.0" - tslib "^2.4.0" - utility-types "^3.10.0" - -"@docusaurus/theme-common@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.4.0.tgz#626096fe9552d240a2115b492c7e12099070cf2d" - integrity sha512-IkG/l5f/FLY6cBIxtPmFnxpuPzc5TupuqlOx+XDN+035MdQcAh8wHXXZJAkTeYDeZ3anIUSUIvWa7/nRKoQEfg== - dependencies: - "@docusaurus/mdx-loader" "2.4.0" - "@docusaurus/module-type-aliases" "2.4.0" - "@docusaurus/plugin-content-blog" "2.4.0" - "@docusaurus/plugin-content-docs" "2.4.0" - "@docusaurus/plugin-content-pages" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-common" "2.4.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - clsx "^1.2.1" - parse-numeric-range "^1.3.0" - prism-react-renderer "^1.3.5" - tslib "^2.4.0" - use-sync-external-store "^1.2.0" - utility-types "^3.10.0" - -"@docusaurus/theme-search-algolia@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.0.tgz#07d297d50c44446d6bc5a37be39afb8f014084e1" - integrity sha512-pPCJSCL1Qt4pu/Z0uxBAuke0yEBbxh0s4fOvimna7TEcBLPq0x06/K78AaABXrTVQM6S0vdocFl9EoNgU17hqA== - dependencies: - "@docsearch/react" "^3.1.1" - "@docusaurus/core" "2.4.0" - "@docusaurus/logger" "2.4.0" - "@docusaurus/plugin-content-docs" "2.4.0" - "@docusaurus/theme-common" "2.4.0" - "@docusaurus/theme-translations" "2.4.0" - "@docusaurus/utils" "2.4.0" - "@docusaurus/utils-validation" "2.4.0" - algoliasearch "^4.13.1" - algoliasearch-helper "^3.10.0" - clsx "^1.2.1" - eta "^2.0.0" - fs-extra "^10.1.0" - lodash "^4.17.21" - tslib "^2.4.0" - utility-types "^3.10.0" - -"@docusaurus/theme-translations@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.4.0.tgz#62dacb7997322f4c5a828b3ab66177ec6769eb33" - integrity sha512-kEoITnPXzDPUMBHk3+fzEzbopxLD3fR5sDoayNH0vXkpUukA88/aDL1bqkhxWZHA3LOfJ3f0vJbOwmnXW5v85Q== - dependencies: - fs-extra "^10.1.0" - tslib "^2.4.0" - -"@docusaurus/types@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.4.0.tgz#f94f89a0253778b617c5d40ac6f16b17ec55ce41" - integrity sha512-xaBXr+KIPDkIaef06c+i2HeTqVNixB7yFut5fBXPGI2f1rrmEV2vLMznNGsFwvZ5XmA3Quuefd4OGRkdo97Dhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - commander "^5.1.0" - joi "^17.6.0" - react-helmet-async "^1.3.0" - utility-types "^3.10.0" - webpack "^5.73.0" - webpack-merge "^5.8.0" - -"@docusaurus/utils-common@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.4.0.tgz#eb2913871860ed32e73858b4c7787dd820c5558d" - integrity sha512-zIMf10xuKxddYfLg5cS19x44zud/E9I7lj3+0bv8UIs0aahpErfNrGhijEfJpAfikhQ8tL3m35nH3hJ3sOG82A== - dependencies: - tslib "^2.4.0" - -"@docusaurus/utils-validation@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.4.0.tgz#1ed92bfab5da321c4a4d99cad28a15627091aa90" - integrity sha512-IrBsBbbAp6y7mZdJx4S4pIA7dUyWSA0GNosPk6ZJ0fX3uYIEQgcQSGIgTeSC+8xPEx3c16o03en1jSDpgQgz/w== - dependencies: - "@docusaurus/logger" "2.4.0" - "@docusaurus/utils" "2.4.0" - joi "^17.6.0" - js-yaml "^4.1.0" - tslib "^2.4.0" - -"@docusaurus/utils@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.4.0.tgz#fdf0c3545819e48bb57eafc5057495fd4d50e900" - integrity sha512-89hLYkvtRX92j+C+ERYTuSUK6nF9bGM32QThcHPg2EDDHVw6FzYQXmX6/p+pU5SDyyx5nBlE4qXR92RxCAOqfg== - dependencies: - "@docusaurus/logger" "2.4.0" - "@svgr/webpack" "^6.2.1" - escape-string-regexp "^4.0.0" - file-loader "^6.2.0" - fs-extra "^10.1.0" - github-slugger "^1.4.0" - globby "^11.1.0" - gray-matter "^4.0.3" - js-yaml "^4.1.0" - lodash "^4.17.21" - micromatch "^4.0.5" - resolve-pathname "^3.0.0" - shelljs "^0.8.5" - tslib "^2.4.0" - url-loader "^4.1.1" - webpack "^5.73.0" - -"@hapi/hoek@^9.0.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@jest/schemas@^29.4.3": - version "29.4.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" - integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== - dependencies: - "@sinclair/typebox" "^0.25.16" - -"@jest/types@^29.5.0": - version "29.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" - integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== - dependencies: - "@jest/schemas" "^29.4.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== - -"@mdx-js/mdx@^1.6.22": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" - integrity sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA== - dependencies: - "@babel/core" "7.12.9" - "@babel/plugin-syntax-jsx" "7.12.1" - "@babel/plugin-syntax-object-rest-spread" "7.8.3" - "@mdx-js/util" "1.6.22" - babel-plugin-apply-mdx-type-prop "1.6.22" - babel-plugin-extract-import-names "1.6.22" - camelcase-css "2.0.1" - detab "2.0.4" - hast-util-raw "6.0.1" - lodash.uniq "4.5.0" - mdast-util-to-hast "10.0.1" - remark-footnotes "2.0.0" - remark-mdx "1.6.22" - remark-parse "8.0.3" - remark-squeeze-paragraphs "4.0.0" - style-to-object "0.3.0" - unified "9.2.0" - unist-builder "2.0.3" - unist-util-visit "2.0.3" - -"@mdx-js/react@^1.6.22": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573" - integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg== - -"@mdx-js/util@1.6.22": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" - integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@polka/url@^1.0.0-next.20": - version "1.0.0-next.21" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" - integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== - -"@sideway/address@^4.1.3": - version "4.1.4" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" - integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@sinclair/typebox@^0.25.16": - version "0.25.24" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" - integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@slorber/static-site-generator-webpack-plugin@^4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3" - integrity sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA== - dependencies: - eval "^0.1.8" - p-map "^4.0.0" - webpack-sources "^3.2.2" - -"@svgr/babel-plugin-add-jsx-attribute@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" - integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== - -"@svgr/babel-plugin-remove-jsx-attribute@*": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-7.0.0.tgz#91da77a009dc38e8d30da45d9b62ef8736f2d90a" - integrity sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ== - -"@svgr/babel-plugin-remove-jsx-empty-expression@*": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-7.0.0.tgz#5154ff1213509e36ab315974c8c2fd48dafb827b" - integrity sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw== - -"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" - integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== - -"@svgr/babel-plugin-svg-dynamic-title@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" - integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== - -"@svgr/babel-plugin-svg-em-dimensions@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" - integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== - -"@svgr/babel-plugin-transform-react-native-svg@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" - integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== - -"@svgr/babel-plugin-transform-svg-component@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" - integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== - -"@svgr/babel-preset@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" - integrity sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^6.5.1" - "@svgr/babel-plugin-remove-jsx-attribute" "*" - "@svgr/babel-plugin-remove-jsx-empty-expression" "*" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.1" - "@svgr/babel-plugin-svg-dynamic-title" "^6.5.1" - "@svgr/babel-plugin-svg-em-dimensions" "^6.5.1" - "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" - "@svgr/babel-plugin-transform-svg-component" "^6.5.1" - -"@svgr/core@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" - integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== - dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" - camelcase "^6.2.0" - cosmiconfig "^7.0.1" - -"@svgr/hast-util-to-babel-ast@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" - integrity sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw== - dependencies: - "@babel/types" "^7.20.0" - entities "^4.4.0" - -"@svgr/plugin-jsx@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" - integrity sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw== - dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/hast-util-to-babel-ast" "^6.5.1" - svg-parser "^2.0.4" - -"@svgr/plugin-svgo@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" - integrity sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ== - dependencies: - cosmiconfig "^7.0.1" - deepmerge "^4.2.2" - svgo "^2.8.0" - -"@svgr/webpack@^6.2.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.1.tgz#ecf027814fc1cb2decc29dc92f39c3cf691e40e8" - integrity sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA== - dependencies: - "@babel/core" "^7.19.6" - "@babel/plugin-transform-react-constant-elements" "^7.18.12" - "@babel/preset-env" "^7.19.4" - "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.18.6" - "@svgr/core" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" - "@svgr/plugin-svgo" "^6.5.1" - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@tsconfig/docusaurus@^1.0.5": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.7.tgz#a3ee3c8109b3fec091e3d61a61834e563aeee3c3" - integrity sha512-ffTXxGIP/IRMCjuzHd6M4/HdIrw1bMfC7Bv8hMkTadnePkpe0lG0oDSdbRpSDZb2rQMAgpbWiR10BvxvNYwYrg== - -"@types/body-parser@*": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== - dependencies: - "@types/node" "*" - -"@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.37.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" - integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" - integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== - -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@*", "@types/express@^4.17.13": - version "4.17.17" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/hast@^2.0.0": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" - integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== - dependencies: - "@types/unist" "*" - -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - -"@types/html-minifier-terser@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" - integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== - -"@types/http-proxy@^1.17.8": - version "1.17.10" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" - integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== - dependencies: - "@types/node" "*" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== - -"@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/mdast@^3.0.0": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0" - integrity sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw== - dependencies: - "@types/unist" "*" - -"@types/mime@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - -"@types/node@*": - version "18.15.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" - integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== - -"@types/node@^17.0.5": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/parse5@^5.0.0": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" - integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== - -"@types/prop-types@*": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== - -"@types/qs@*": - version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - -"@types/react-router-config@*", "@types/react-router-config@^5.0.6": - version "5.0.6" - resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451" - integrity sha512-db1mx37a1EJDf1XeX8jJN7R3PZABmJQXR8r28yUjVMFSjkmnQo6X6pOEEmNl+Tp2gYQOGPdYbFIipBtdElZ3Yg== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router-dom@*": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - -"@types/react@*": - version "18.0.31" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.31.tgz#a69ef8dd7bfa849734d258c793a8fe343a338205" - integrity sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/sax@^1.2.1": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.4.tgz#8221affa7f4f3cb21abd22f244cfabfa63e6a69e" - integrity sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw== - dependencies: - "@types/node" "*" - -"@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== - -"@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.1" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" - integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== - dependencies: - "@types/mime" "*" - "@types/node" "*" - -"@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== - dependencies: - "@types/node" "*" - -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== - -"@types/ws@^8.5.1": - version "8.5.4" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== - dependencies: - "@types/node" "*" - -"@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== - -"@types/yargs@^17.0.8": - version "17.0.24" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" - integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== - dependencies: - "@types/yargs-parser" "*" - -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== - -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== - -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== - -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== - -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== - -acorn-walk@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -address@^1.0.1, address@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" - integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -algoliasearch-helper@^3.10.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.12.0.tgz#0fe39d49b0290e4aa5e1fe733bd24d857d258e94" - integrity sha512-/j1U3PEwdan0n6P/QqSnSpNSLC5+cEMvyljd5CnmNmUjDlGrys+vFEOwjVEnqELIiAGMHEA/Nl3CiKVFBUYqyQ== - dependencies: - "@algolia/events" "^4.0.1" - -algoliasearch@^4.0.0, algoliasearch@^4.13.1: - version "4.16.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.16.0.tgz#2807f1aee8a574fa8480f39a09b16e407d4cc1a6" - integrity sha512-HAjKJ6bBblaXqO4dYygF4qx251GuJ6zCZt+qbJ+kU7sOC+yc84pawEjVpJByh+cGP2APFCsao2Giz50cDlKNPA== - dependencies: - "@algolia/cache-browser-local-storage" "4.16.0" - "@algolia/cache-common" "4.16.0" - "@algolia/cache-in-memory" "4.16.0" - "@algolia/client-account" "4.16.0" - "@algolia/client-analytics" "4.16.0" - "@algolia/client-common" "4.16.0" - "@algolia/client-personalization" "4.16.0" - "@algolia/client-search" "4.16.0" - "@algolia/logger-common" "4.16.0" - "@algolia/logger-console" "4.16.0" - "@algolia/requester-browser-xhr" "4.16.0" - "@algolia/requester-common" "4.16.0" - "@algolia/requester-node-http" "4.16.0" - "@algolia/transporter" "4.16.0" - -ansi-align@^3.0.0, ansi-align@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^10.4.12, autoprefixer@^10.4.7: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-loader@^8.2.5: - version "8.3.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" - integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - -babel-plugin-apply-mdx-type-prop@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz#d216e8fd0de91de3f1478ef3231e05446bc8705b" - integrity sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ== - dependencies: - "@babel/helper-plugin-utils" "7.10.4" - "@mdx-js/util" "1.6.22" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-extract-import-names@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz#de5f9a28eb12f3eb2578bf74472204e66d1a13dc" - integrity sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ== - dependencies: - "@babel/helper-plugin-utils" "7.10.4" - -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" - -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - -bail@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" - integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base16@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" - integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.0.11: - version "1.1.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135" - integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== - dependencies: - array-flatten "^2.1.2" - dns-equal "^1.0.0" - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -boxen@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" - integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^6.2.0" - chalk "^4.1.0" - cli-boxes "^2.2.1" - string-width "^4.2.2" - type-fest "^0.20.2" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - -boxen@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d" - integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== - dependencies: - ansi-align "^3.0.1" - camelcase "^6.2.0" - chalk "^4.1.2" - cli-boxes "^3.0.0" - string-width "^5.0.1" - type-fest "^2.5.0" - widest-line "^4.0.1" - wrap-ansi "^8.0.1" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase-css@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001473" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001473.tgz#3859898b3cab65fc8905bb923df36ad35058153c" - integrity sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg== - -ccount@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" - integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -character-entities-legacy@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" - integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== - -character-entities@^1.0.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" - integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== - -character-reference-invalid@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" - integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== - -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@^1.0.0-rc.12: - version "1.0.0-rc.12" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" - integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - -chokidar@^3.4.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== - -clean-css@^5.2.2, clean-css@^5.3.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" - integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - -cli-boxes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" - integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== - -cli-table3@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone-response@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" - integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== - dependencies: - mimic-response "^1.0.0" - -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -collapse-white-space@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" - integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^2.0.10: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - -combine-promises@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71" - integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg== - -comma-separated-tokens@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" - integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -copy-text-to-clipboard@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz#6bf40deef0a51ac6858efb0d76ded2c6d6a15059" - integrity sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng== - -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-js-compat@^3.25.1: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.1.tgz#15c0fb812ea27c973c18d425099afa50b934b41b" - integrity sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA== - dependencies: - browserslist "^4.21.5" - -core-js-pure@^3.25.1: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.29.1.tgz#1be6ca2b8772f6b4df7fc4621743286e676c6162" - integrity sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg== - -core-js@^3.23.3: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.1.tgz#40ff3b41588b091aaed19ca1aa5cb111803fa9a6" - integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -cosmiconfig@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -cosmiconfig@^8.0.0: - version "8.1.3" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.3.tgz#0e614a118fcc2d9e5afc2f87d53cd09931015689" - integrity sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw== - dependencies: - import-fresh "^3.2.1" - js-yaml "^4.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -css-declaration-sorter@^6.3.1: - version "6.4.0" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad" - integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew== - -css-loader@^6.7.1: - version "6.7.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" - integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.19" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.3.8" - -css-minimizer-webpack-plugin@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35" - integrity sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA== - dependencies: - cssnano "^5.1.8" - jest-worker "^29.1.2" - postcss "^8.4.17" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^6.0.1, css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-advanced@^5.3.8: - version "5.3.10" - resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz#25558a1fbf3a871fb6429ce71e41be7f5aca6eef" - integrity sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ== - dependencies: - autoprefixer "^10.4.12" - cssnano-preset-default "^5.2.14" - postcss-discard-unused "^5.1.0" - postcss-merge-idents "^5.1.1" - postcss-reduce-idents "^5.2.0" - postcss-zindex "^5.1.0" - -cssnano-preset-default@^5.2.14: - version "5.2.14" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" - integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== - dependencies: - css-declaration-sorter "^6.3.1" - cssnano-utils "^3.1.0" - postcss-calc "^8.2.3" - postcss-colormin "^5.3.1" - postcss-convert-values "^5.1.3" - postcss-discard-comments "^5.1.2" - postcss-discard-duplicates "^5.1.0" - postcss-discard-empty "^5.1.1" - postcss-discard-overridden "^5.1.0" - postcss-merge-longhand "^5.1.7" - postcss-merge-rules "^5.1.4" - postcss-minify-font-values "^5.1.0" - postcss-minify-gradients "^5.1.1" - postcss-minify-params "^5.1.4" - postcss-minify-selectors "^5.2.1" - postcss-normalize-charset "^5.1.0" - postcss-normalize-display-values "^5.1.0" - postcss-normalize-positions "^5.1.1" - postcss-normalize-repeat-style "^5.1.1" - postcss-normalize-string "^5.1.0" - postcss-normalize-timing-functions "^5.1.0" - postcss-normalize-unicode "^5.1.1" - postcss-normalize-url "^5.1.0" - postcss-normalize-whitespace "^5.1.1" - postcss-ordered-values "^5.1.3" - postcss-reduce-initial "^5.1.2" - postcss-reduce-transforms "^5.1.0" - postcss-svgo "^5.1.0" - postcss-unique-selectors "^5.1.1" - -cssnano-utils@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" - integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== - -cssnano@^5.1.12, cssnano@^5.1.8: - version "5.1.15" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" - integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== - dependencies: - cssnano-preset-default "^5.2.14" - lilconfig "^2.0.3" - yaml "^1.10.2" - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== - -debug@2.6.9, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.0, debug@^4.1.1: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== - dependencies: - mimic-response "^1.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detab@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43" - integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g== - dependencies: - repeat-string "^1.5.4" - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -detect-port@^1.3.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.5.1.tgz#451ca9b6eaf20451acb0799b8ab40dff7718727b" - integrity sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ== - dependencies: - address "^1.0.1" - debug "4" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== - -dns-packet@^5.2.2: - version "5.5.0" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.5.0.tgz#f59cbf3396c130957c56a6ad5fd3959ccdc30065" - integrity sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -dom-converter@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -domutils@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" - integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.1" - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.4.284: - version "1.4.348" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.348.tgz#f49379dc212d79f39112dd026f53e371279e433d" - integrity sha512-gM7TdwuG3amns/1rlgxMbeeyNoBFPa+4Uu0c7FeROWh4qWmvSOnvcslKmWy51ggLKZ2n/F/4i2HJ+PVNxH9uCQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -emoticon@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" - integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.2.0, entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - -escape-html@^1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eta@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/eta/-/eta-2.0.1.tgz#199e675359cb6e19d38f29e1f405e1ba0e79a6df" - integrity sha512-46E2qDPDm7QA+usjffUWz9KfXsxVZclPOuKsXs4ZWZdI/X1wpDF7AO424pt7fdYohCzWsIkXAhNGXSlwo5naAg== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eval@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" - integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== - dependencies: - "@types/node" "*" - require-like ">= 0.1.1" - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - -fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fbemitter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-3.0.0.tgz#00b2a1af5411254aab416cd75f9e6289bee4bff3" - integrity sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw== - dependencies: - fbjs "^3.0.0" - -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - -fbjs@^3.0.0, fbjs@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6" - integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== - dependencies: - cross-fetch "^3.1.5" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.30" - -feed@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" - integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== - dependencies: - xml-js "^1.6.11" - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-cache-dir@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flux@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/flux/-/flux-4.0.4.tgz#9661182ea81d161ee1a6a6af10d20485ef2ac572" - integrity sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw== - dependencies: - fbemitter "^3.0.0" - fbjs "^3.0.1" - -follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-extra@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-monkey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -github-slugger@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" - integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globby@^13.1.1: - version "13.1.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" - integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^4.0.0" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -gray-matter@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" - integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== - dependencies: - js-yaml "^3.13.1" - kind-of "^6.0.2" - section-matter "^1.0.0" - strip-bom-string "^1.0.0" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hast-to-hyperscript@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" - integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== - dependencies: - "@types/unist" "^2.0.3" - comma-separated-tokens "^1.0.0" - property-information "^5.3.0" - space-separated-tokens "^1.0.0" - style-to-object "^0.3.0" - unist-util-is "^4.0.0" - web-namespaces "^1.0.0" - -hast-util-from-parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" - integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== - dependencies: - "@types/parse5" "^5.0.0" - hastscript "^6.0.0" - property-information "^5.0.0" - vfile "^4.0.0" - vfile-location "^3.2.0" - web-namespaces "^1.0.0" - -hast-util-parse-selector@^2.0.0: - version "2.2.5" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" - integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== - -hast-util-raw@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.0.1.tgz#973b15930b7529a7b66984c98148b46526885977" - integrity sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig== - dependencies: - "@types/hast" "^2.0.0" - hast-util-from-parse5 "^6.0.0" - hast-util-to-parse5 "^6.0.0" - html-void-elements "^1.0.0" - parse5 "^6.0.0" - unist-util-position "^3.0.0" - vfile "^4.0.0" - web-namespaces "^1.0.0" - xtend "^4.0.0" - zwitch "^1.0.0" - -hast-util-to-parse5@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" - integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== - dependencies: - hast-to-hyperscript "^9.0.0" - property-information "^5.0.0" - web-namespaces "^1.0.0" - xtend "^4.0.0" - zwitch "^1.0.0" - -hastscript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" - integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" - integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== - -html-minifier-terser@^6.0.2, html-minifier-terser@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" - integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== - dependencies: - camel-case "^4.1.2" - clean-css "^5.2.2" - commander "^8.3.0" - he "^1.2.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.10.0" - -html-tags@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" - integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== - -html-void-elements@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" - integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== - -html-webpack-plugin@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" - integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw== - dependencies: - "@types/html-minifier-terser" "^6.0.0" - html-minifier-terser "^6.0.2" - lodash "^4.17.21" - pretty-error "^4.0.0" - tapable "^2.0.0" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -image-size@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486" - integrity sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg== - dependencies: - queue "6.0.2" - -immer@^9.0.7: - version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" - integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== - -import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - -is-alphabetical@1.0.4, is-alphabetical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" - integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== - -is-alphanumerical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" - integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== - dependencies: - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== - dependencies: - has "^1.0.3" - -is-decimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" - integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" - integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-npm@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" - integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-whitespace-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" - integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== - -is-word-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" - integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jest-util@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" - integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== - dependencies: - "@jest/types" "^29.5.0" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.1.2: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d" - integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== - dependencies: - "@types/node" "*" - jest-util "^29.5.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -joi@^17.6.0: - version "17.9.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.1.tgz#74899b9fa3646904afa984a11df648eca66c9018" - integrity sha512-FariIi9j6QODKATGBrEX7HZcja8Bsh3rfdGYy/Sb65sGlZWK/QWesU1ghk7aJWDj95knjXlQfSmzFSPPkLVsfw== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.1.2, json5@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -klona@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -latest-version@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.7.3" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lilconfig@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.curry@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" - integrity sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.flow@^3.3.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" - integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.uniq@4.5.0, lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -markdown-escapes@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" - integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== - -mdast-squeeze-paragraphs@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" - integrity sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ== - dependencies: - unist-util-remove "^2.0.0" - -mdast-util-definitions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" - integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== - dependencies: - unist-util-visit "^2.0.0" - -mdast-util-to-hast@10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb" - integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - mdast-util-definitions "^4.0.0" - mdurl "^1.0.0" - unist-builder "^2.0.0" - unist-util-generated "^1.0.0" - unist-util-position "^3.0.0" - unist-util-visit "^2.0.0" - -mdast-util-to-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" - integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdurl@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^3.1.2, memfs@^3.4.3: - version "3.4.13" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345" - integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg== - dependencies: - fs-monkey "^1.0.3" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mini-css-extract-plugin@^2.6.1: - version "2.7.5" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5" - integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ== - dependencies: - schema-utils "^4.0.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mrmime@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -nanoid@^3.3.4: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-emoji@^1.10.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" - integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== - dependencies: - lodash "^4.17.21" - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nprogress@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" - integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9, open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" - integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-numeric-range@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" - integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== - -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== - dependencies: - domhandler "^5.0.2" - parse5 "^7.0.0" - -parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - -postcss-calc@^8.2.3: - version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" - integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== - dependencies: - postcss-selector-parser "^6.0.9" - postcss-value-parser "^4.2.0" - -postcss-colormin@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" - integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - colord "^2.9.1" - postcss-value-parser "^4.2.0" - -postcss-convert-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" - integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-discard-comments@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" - integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== - -postcss-discard-duplicates@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" - integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== - -postcss-discard-empty@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" - integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== - -postcss-discard-overridden@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" - integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== - -postcss-discard-unused@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz#8974e9b143d887677304e558c1166d3762501142" - integrity sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-loader@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.1.0.tgz#3ba0dfddff06043f3eac7690a1d8b432264bb866" - integrity sha512-vTD2DJ8vJD0Vr1WzMQkRZWRjcynGh3t7NeoLg+Sb1TeuK7etiZfL/ZwHbaVa3M+Qni7Lj/29voV9IggnIUjlIw== - dependencies: - cosmiconfig "^8.0.0" - klona "^2.0.6" - semver "^7.3.8" - -postcss-merge-idents@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz#7753817c2e0b75d0853b56f78a89771e15ca04a1" - integrity sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw== - dependencies: - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-merge-longhand@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" - integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== - dependencies: - postcss-value-parser "^4.2.0" - stylehacks "^5.1.1" - -postcss-merge-rules@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" - integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - cssnano-utils "^3.1.0" - postcss-selector-parser "^6.0.5" - -postcss-minify-font-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" - integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-minify-gradients@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" - integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== - dependencies: - colord "^2.9.1" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-params@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" - integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== - dependencies: - browserslist "^4.21.4" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-selectors@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" - integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-normalize-charset@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" - integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== - -postcss-normalize-display-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" - integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-positions@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" - integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-repeat-style@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" - integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-string@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" - integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-timing-functions@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" - integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-unicode@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" - integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-normalize-url@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" - integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== - dependencies: - normalize-url "^6.0.1" - postcss-value-parser "^4.2.0" - -postcss-normalize-whitespace@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" - integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-ordered-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" - integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== - dependencies: - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-reduce-idents@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz#c89c11336c432ac4b28792f24778859a67dfba95" - integrity sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-reduce-initial@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" - integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" - integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sort-media-queries@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-4.3.0.tgz#f48a77d6ce379e86676fc3f140cf1b10a06f6051" - integrity sha512-jAl8gJM2DvuIJiI9sL1CuiHtKM4s5aEIomkU8G3LFvbP+p8i7Sz8VV63uieTgoewGqKbi+hxBTiOKJlB35upCg== - dependencies: - sort-css-media-queries "2.1.0" - -postcss-svgo@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" - integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== - dependencies: - postcss-value-parser "^4.2.0" - svgo "^2.7.0" - -postcss-unique-selectors@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" - integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss-zindex@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" - integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== - -postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.19: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== - -pretty-error@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" - integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== - dependencies: - lodash "^4.17.20" - renderkid "^3.0.0" - -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - -prism-react-renderer@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085" - integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg== - -prismjs@^1.28.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" - integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.6.2, prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -property-information@^5.0.0, property-information@^5.3.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" - integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== - dependencies: - xtend "^4.0.0" - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - -pupa@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" - integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== - dependencies: - escape-goat "^2.0.0" - -pure-color@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" - integrity sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA== - -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@1.2.8, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-base16-styling@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" - integrity sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ== - dependencies: - base16 "^1.0.0" - lodash.curry "^4.0.1" - lodash.flow "^3.3.0" - pure-color "^1.2.0" - -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f" - integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg== - -react-helmet-async@*, react-helmet-async@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== - dependencies: - "@babel/runtime" "^7.12.5" - invariant "^2.2.4" - prop-types "^15.7.2" - react-fast-compare "^3.2.0" - shallowequal "^1.1.0" - -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-json-view@^1.21.3: - version "1.21.3" - resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.21.3.tgz#f184209ee8f1bf374fb0c41b0813cff54549c475" - integrity sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw== - dependencies: - flux "^4.0.1" - react-base16-styling "^0.6.0" - react-lifecycles-compat "^3.0.4" - react-textarea-autosize "^8.3.2" - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-loadable-ssr-addon-v5-slorber@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" - integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== - dependencies: - "@babel/runtime" "^7.10.3" - -react-router-config@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" - integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== - dependencies: - "@babel/runtime" "^7.1.2" - -react-router-dom@^5.3.3: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4, react-router@^5.3.3: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-textarea-autosize@^8.3.2: - version "8.4.1" - resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz#bcfc5462727014b808b14ee916c01e275e8a8335" - integrity sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q== - dependencies: - "@babel/runtime" "^7.20.13" - use-composed-ref "^1.3.0" - use-latest "^1.2.1" - -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -registry-auth-token@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" - integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== - dependencies: - rc "1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== - -remark-emoji@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.2.0.tgz#1c702090a1525da5b80e15a8f963ef2c8236cac7" - integrity sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w== - dependencies: - emoticon "^3.2.0" - node-emoji "^1.10.0" - unist-util-visit "^2.0.3" - -remark-footnotes@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-2.0.0.tgz#9001c4c2ffebba55695d2dd80ffb8b82f7e6303f" - integrity sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ== - -remark-mdx@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.6.22.tgz#06a8dab07dcfdd57f3373af7f86bd0e992108bbd" - integrity sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ== - dependencies: - "@babel/core" "7.12.9" - "@babel/helper-plugin-utils" "7.10.4" - "@babel/plugin-proposal-object-rest-spread" "7.12.1" - "@babel/plugin-syntax-jsx" "7.12.1" - "@mdx-js/util" "1.6.22" - is-alphabetical "1.0.4" - remark-parse "8.0.3" - unified "9.2.0" - -remark-parse@8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" - integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q== - dependencies: - ccount "^1.0.0" - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^2.0.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim "0.0.1" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^2.0.0" - vfile-location "^3.0.0" - xtend "^4.0.1" - -remark-squeeze-paragraphs@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" - integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw== - dependencies: - mdast-squeeze-paragraphs "^4.0.0" - -renderkid@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" - integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== - dependencies: - css-select "^4.1.3" - dom-converter "^0.2.0" - htmlparser2 "^6.1.0" - lodash "^4.17.21" - strip-ansi "^6.0.1" - -repeat-string@^1.5.4: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" - integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve@^1.1.6, resolve@^1.14.2, resolve@^1.3.2: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== - dependencies: - lowercase-keys "^1.0.0" - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rtl-detect@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" - integrity sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ== - -rtlcss@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-3.5.0.tgz#c9eb91269827a102bac7ae3115dd5d049de636c3" - integrity sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A== - dependencies: - find-up "^5.0.0" - picocolors "^1.0.0" - postcss "^8.3.11" - strip-json-comments "^3.1.1" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@^7.5.4: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== - dependencies: - tslib "^2.1.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.8.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" - -section-matter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" - integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== - dependencies: - extend-shallow "^2.0.1" - kind-of "^6.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== - dependencies: - node-forge "^1" - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== - dependencies: - randombytes "^2.1.0" - -serve-handler@^6.1.3: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3: - version "1.8.0" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" - integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== - -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sirv@^1.0.7: - version "1.0.19" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" - integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== - dependencies: - "@polka/url" "^1.0.0-next.20" - mrmime "^1.0.0" - totalist "^1.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -sitemap@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -sort-css-media-queries@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" - integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -space-separated-tokens@^1.0.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" - integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -state-toggle@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" - integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -std-env@^3.0.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955" - integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -style-to-object@0.3.0, style-to-object@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" - integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== - dependencies: - inline-style-parser "0.1.1" - -stylehacks@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" - integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== - dependencies: - browserslist "^4.21.4" - postcss-selector-parser "^6.0.4" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-parser@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - -svgo@^2.7.0, svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.3: - version "5.3.7" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" - integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.5" - -terser@^5.10.0, terser@^5.16.5: - version "5.16.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.8.tgz#ccde583dabe71df3f4ed02b65eb6532e0fae15d5" - integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -tiny-invariant@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -totalist@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" - integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-trailing-lines@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" - integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== - -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ== - -trough@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" - integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== - -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^2.5.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typescript@^4.7.4: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - -ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== - -unherit@^1.0.4: - version "1.1.3" - resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" - integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== - dependencies: - inherits "^2.0.0" - xtend "^4.0.0" - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unified@9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8" - integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^2.0.0" - trough "^1.0.0" - vfile "^4.0.0" - -unified@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" - integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^2.0.0" - trough "^1.0.0" - vfile "^4.0.0" - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unist-builder@2.0.3, unist-builder@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" - integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== - -unist-util-generated@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" - integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== - -unist-util-is@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" - integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== - -unist-util-position@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" - integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== - -unist-util-remove-position@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" - integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== - dependencies: - unist-util-visit "^2.0.0" - -unist-util-remove@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.1.0.tgz#b0b4738aa7ee445c402fda9328d604a02d010588" - integrity sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q== - dependencies: - unist-util-is "^4.0.0" - -unist-util-stringify-position@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" - integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== - dependencies: - "@types/unist" "^2.0.2" - -unist-util-visit-parents@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" - integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - -unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" - integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -update-notifier@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" - integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== - dependencies: - boxen "^5.0.0" - chalk "^4.1.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.4.0" - is-npm "^5.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.1.0" - pupa "^2.1.1" - semver "^7.3.4" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-loader@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" - integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.27" - schema-utils "^3.0.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== - dependencies: - prepend-http "^2.0.0" - -use-composed-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" - integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== - -use-isomorphic-layout-effect@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" - integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== - -use-latest@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" - integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw== - dependencies: - use-isomorphic-layout-effect "^1.1.1" - -use-sync-external-store@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" - integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== - -utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vfile-location@^3.0.0, vfile-location@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" - integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== - -vfile-message@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" - integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^2.0.0" - -vfile@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" - integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^2.0.0" - vfile-message "^2.0.0" - -wait-on@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" - integrity sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw== - dependencies: - axios "^0.25.0" - joi "^17.6.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.5.4" - -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -web-namespaces@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" - integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webpack-bundle-analyzer@^4.5.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz#951b8aaf491f665d2ae325d8b84da229157b1d04" - integrity sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg== - dependencies: - "@discoveryjs/json-ext" "0.5.7" - acorn "^8.0.4" - acorn-walk "^8.0.0" - chalk "^4.1.0" - commander "^7.2.0" - gzip-size "^6.0.0" - lodash "^4.17.20" - opener "^1.5.2" - sirv "^1.0.7" - ws "^7.3.1" - -webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^4.9.3: - version "4.13.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz#d97445481d78691efe6d9a3b230833d802fc31f9" - integrity sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw== - dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.1" - ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^5.3.1" - ws "^8.13.0" - -webpack-merge@^5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^3.2.2, webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack@^5.73.0: - version "5.77.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.77.0.tgz#dea3ad16d7ea6b84aa55fa42f4eac9f30e7eb9b4" - integrity sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.1.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" - webpack-sources "^3.2.3" - -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== - dependencies: - chalk "^4.1.0" - consola "^2.15.3" - pretty-time "^1.1.0" - std-env "^3.0.1" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -widest-line@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" - integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== - dependencies: - string-width "^5.0.1" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@^7.3.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== - -ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -xtend@^4.0.0, xtend@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zwitch@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" - integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 000000000..a146c92cf --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + ], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-explicit-any": "off", + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..8bbabc2ca --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20.12-alpine + +WORKDIR /app + +COPY ./frontend ./frontend +COPY ./client/core/ts ./client + +ARG VITE_MONITOR_HOST +ENV VITE_MONITOR_HOST ${VITE_MONITOR_HOST} + +RUN cd client && yarn && yarn build && yarn link +RUN cd frontend && yarn link @monitor/client && yarn && yarn build + +ENV PORT 4174 + +CMD cd frontend && yarn preview --host --port ${PORT} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..1ebe379f5 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 000000000..c73b5b13d --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/", + "utils": "@lib/utils" + } +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 7b5f53982..ee9b244ea 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,26 +1,20 @@ - - - - - - - - - - - - - - - monitor - - - - -
- - - - - - + + + + + + + + + + + + + + Monitor + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json index 751537a9b..35f4a0696 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,31 +1,61 @@ -{ - "name": "vite-template-solid", - "version": "0.0.0", - "description": "", - "scripts": { - "start": "vite", - "dev": "vite", - "check": "tsc", - "build": "vite build && node post-build.mjs", - "serve": "vite preview" - }, - "license": "GPL v3.0", - "devDependencies": { - "@types/sanitize-html": "^2.9.0", - "sass": "^1.57.1", - "typescript": "^4.9.4", - "vite": "^4.0.3", - "vite-plugin-solid": "^2.5.0" - }, - "dependencies": { - "@solidjs/router": "^0.6.0", - "@tanstack/solid-query": "^4.26.0", - "ansi-to-html": "^0.7.2", - "axios": "^1.2.1", - "js-file-download": "^0.4.12", - "lightweight-charts": "^3.8.0", - "reconnecting-websocket": "^4.4.0", - "sanitize-html": "^2.10.0", - "solid-js": "^1.6.6" - } -} +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "regen-client": "cd ../client/ts && yarn build" + }, + "dependencies": { + "@radix-ui/react-checkbox": "1.0.4", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-dropdown-menu": "2.0.6", + "@radix-ui/react-icons": "1.3.0", + "@radix-ui/react-label": "2.0.2", + "@radix-ui/react-popover": "1.0.7", + "@radix-ui/react-progress": "1.0.3", + "@radix-ui/react-select": "2.0.0", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-switch": "1.0.3", + "@radix-ui/react-tabs": "1.0.4", + "@radix-ui/react-toast": "1.1.5", + "@tanstack/react-query": "5.29.2", + "@tanstack/react-table": "8.16.0", + "ansi-to-html": "0.7.2", + "class-variance-authority": "0.7.0", + "clsx": "2.1.0", + "cmdk": "1.0.0", + "jotai": "2.8.0", + "lightweight-charts": "4.1.3", + "lucide-react": "0.371.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-minimal-pie-chart": "8.4.0", + "react-router-dom": "6.22.3", + "reconnecting-websocket": "4.4.0", + "sanitize-html": "2.13.0", + "tailwind-merge": "2.2.2", + "tailwindcss-animate": "1.0.7" + }, + "devDependencies": { + "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", + "@types/sanitize-html": "2.11.0", + "@typescript-eslint/eslint-plugin": "7.7.0", + "@typescript-eslint/parser": "7.7.0", + "@vitejs/plugin-react": "4.2.1", + "autoprefixer": "10.4.19", + "eslint": "9.0.0", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-react-refresh": "0.4.6", + "postcss": "8.4.38", + "tailwindcss": "3.4.3", + "typescript": "5.4.5", + "vite": "5.2.10", + "vite-tsconfig-paths": "4.3.2" + } +} diff --git a/frontend/post-build.mjs b/frontend/post-build.mjs deleted file mode 100644 index 17eb73415..000000000 --- a/frontend/post-build.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { readdirSync, renameSync, rmdirSync } from "fs"; - -const files = readdirSync("./build/assets"); - -for (const file of files) { - renameSync("./build/assets/" + file, "./build/" + file); -} - -rmdirSync("./build/assets"); - -console.log("\npost build complete\n") \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/icons/application.svg b/frontend/public/icons/application.svg deleted file mode 100644 index fa01487ca..000000000 --- a/frontend/public/icons/application.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/arrow-down.svg b/frontend/public/icons/arrow-down.svg deleted file mode 100644 index 67ec4a99b..000000000 --- a/frontend/public/icons/arrow-down.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - -Shape -Created with Sketch. - - - - - - diff --git a/frontend/public/icons/arrow-left.svg b/frontend/public/icons/arrow-left.svg deleted file mode 100644 index 4ea6f30ac..000000000 --- a/frontend/public/icons/arrow-left.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/arrow-right.svg b/frontend/public/icons/arrow-right.svg deleted file mode 100644 index 44bf17c06..000000000 --- a/frontend/public/icons/arrow-right.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/arrow-up.svg b/frontend/public/icons/arrow-up.svg deleted file mode 100644 index 629f6fb10..000000000 --- a/frontend/public/icons/arrow-up.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - -Shape -Created with Sketch. - - - - - - diff --git a/frontend/public/icons/build.svg b/frontend/public/icons/build.svg deleted file mode 100644 index 9dca545bb..000000000 --- a/frontend/public/icons/build.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/caret-right.svg b/frontend/public/icons/caret-right.svg deleted file mode 100644 index 2899b42db..000000000 --- a/frontend/public/icons/caret-right.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/check.svg b/frontend/public/icons/check.svg deleted file mode 100644 index a72c54ea1..000000000 --- a/frontend/public/icons/check.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/chevron-down.svg b/frontend/public/icons/chevron-down.svg deleted file mode 100644 index ed08a9447..000000000 --- a/frontend/public/icons/chevron-down.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/chevron-left.svg b/frontend/public/icons/chevron-left.svg deleted file mode 100644 index 845af34a3..000000000 --- a/frontend/public/icons/chevron-left.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/chevron-right.svg b/frontend/public/icons/chevron-right.svg deleted file mode 100644 index 052ec0249..000000000 --- a/frontend/public/icons/chevron-right.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/chevron-up.svg b/frontend/public/icons/chevron-up.svg deleted file mode 100644 index 70bcaa798..000000000 --- a/frontend/public/icons/chevron-up.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/clipboard.svg b/frontend/public/icons/clipboard.svg deleted file mode 100644 index 4d3477717..000000000 --- a/frontend/public/icons/clipboard.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/cog.svg b/frontend/public/icons/cog.svg deleted file mode 100644 index 2842f0b64..000000000 --- a/frontend/public/icons/cog.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/command.svg b/frontend/public/icons/command.svg deleted file mode 100644 index 074ac519c..000000000 --- a/frontend/public/icons/command.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - command - - - - diff --git a/frontend/public/icons/console.svg b/frontend/public/icons/console.svg deleted file mode 100644 index ea9bb764d..000000000 --- a/frontend/public/icons/console.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/frontend/public/icons/cross.svg b/frontend/public/icons/cross.svg deleted file mode 100644 index 14f5fe0a3..000000000 --- a/frontend/public/icons/cross.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/cut.svg b/frontend/public/icons/cut.svg deleted file mode 100644 index c25951e5d..000000000 --- a/frontend/public/icons/cut.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/double-chevron-right.svg b/frontend/public/icons/double-chevron-right.svg deleted file mode 100644 index d61e1fea7..000000000 --- a/frontend/public/icons/double-chevron-right.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/duplicate.svg b/frontend/public/icons/duplicate.svg deleted file mode 100644 index b8bbb2c3f..000000000 --- a/frontend/public/icons/duplicate.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/edit.svg b/frontend/public/icons/edit.svg deleted file mode 100644 index aec04099c..000000000 --- a/frontend/public/icons/edit.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/error.svg b/frontend/public/icons/error.svg deleted file mode 100644 index 7a389a0c0..000000000 --- a/frontend/public/icons/error.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/exchange.svg b/frontend/public/icons/exchange.svg deleted file mode 100644 index 6cdef3ede..000000000 --- a/frontend/public/icons/exchange.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/eye-off.svg b/frontend/public/icons/eye-off.svg deleted file mode 100644 index f38d9f2b0..000000000 --- a/frontend/public/icons/eye-off.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/eye-open.svg b/frontend/public/icons/eye-open.svg deleted file mode 100644 index 51732c7e4..000000000 --- a/frontend/public/icons/eye-open.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - z - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/frontend/public/icons/floppy-disk.svg b/frontend/public/icons/floppy-disk.svg deleted file mode 100644 index 1f77a4953..000000000 --- a/frontend/public/icons/floppy-disk.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/fullscreen.svg b/frontend/public/icons/fullscreen.svg deleted file mode 100644 index 8022c2176..000000000 --- a/frontend/public/icons/fullscreen.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/home.svg b/frontend/public/icons/home.svg deleted file mode 100644 index 0987dddd3..000000000 --- a/frontend/public/icons/home.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/info-sign.svg b/frontend/public/icons/info-sign.svg deleted file mode 100644 index 9646bb0a4..000000000 --- a/frontend/public/icons/info-sign.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/log.svg b/frontend/public/icons/log.svg deleted file mode 100644 index 701b7e376..000000000 --- a/frontend/public/icons/log.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - log - - - - diff --git a/frontend/public/icons/menu.svg b/frontend/public/icons/menu.svg deleted file mode 100644 index b29931956..000000000 --- a/frontend/public/icons/menu.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/minus.svg b/frontend/public/icons/minus.svg deleted file mode 100644 index cb4edeb86..000000000 --- a/frontend/public/icons/minus.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/notifications.svg b/frontend/public/icons/notifications.svg deleted file mode 100644 index 4ab10a265..000000000 --- a/frontend/public/icons/notifications.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/pause.svg b/frontend/public/icons/pause.svg deleted file mode 100644 index 470ea5d38..000000000 --- a/frontend/public/icons/pause.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/play.svg b/frontend/public/icons/play.svg deleted file mode 100644 index 11e8d1387..000000000 --- a/frontend/public/icons/play.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/plus.svg b/frontend/public/icons/plus.svg deleted file mode 100644 index a96d61bc8..000000000 --- a/frontend/public/icons/plus.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/icons/refresh.svg b/frontend/public/icons/refresh.svg deleted file mode 100644 index e109ccb41..000000000 --- a/frontend/public/icons/refresh.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/reset.svg b/frontend/public/icons/reset.svg deleted file mode 100644 index ad0a4a33a..000000000 --- a/frontend/public/icons/reset.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - reset - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/frontend/public/icons/search.svg b/frontend/public/icons/search.svg deleted file mode 100644 index 74ec09e46..000000000 --- a/frontend/public/icons/search.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/star-empty.svg b/frontend/public/icons/star-empty.svg deleted file mode 100644 index da1383e9f..000000000 --- a/frontend/public/icons/star-empty.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/frontend/public/icons/star.svg b/frontend/public/icons/star.svg deleted file mode 100644 index e26882435..000000000 --- a/frontend/public/icons/star.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/frontend/public/icons/timeline-line-chart.svg b/frontend/public/icons/timeline-line-chart.svg deleted file mode 100644 index 076f93ac2..000000000 --- a/frontend/public/icons/timeline-line-chart.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/trash.svg b/frontend/public/icons/trash.svg deleted file mode 100644 index 01c49aaf9..000000000 --- a/frontend/public/icons/trash.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/icons/user.svg b/frontend/public/icons/user.svg deleted file mode 100644 index cbbc7f62d..000000000 --- a/frontend/public/icons/user.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index bf1908d01..2a7e03f10 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "monitor", - "name": "mogh tech monitor", + "short_name": "Monitor", + "name": "Monitor", "icons": [ { "src": "favicon.ico", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 540d41d1c..000000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Route, Routes } from "@solidjs/router"; -import { Component, lazy, Show } from "solid-js"; -import Topbar from "./components/topbar/Topbar"; -import { useUser } from "./state/UserProvider"; - -const Home = lazy(() => import("./components/home/Home")); -const Deployment = lazy(() => import("./components/deployment/Deployment")); -const Server = lazy(() => import("./components/server/Server")); -const Build = lazy(() => import("./components/build/Build")); -const Users = lazy(() => import("./components/users/Users")); -const User = lazy(() => import("./components/users/User")); -const Stats = lazy(() => import("./components/stats/Stats")); -const Account = lazy(() => import("./components/account/Account")); -const Updates = lazy(() => import("./components/Updates")); - -const App: Component = () => { - const { user } = useUser(); - return ( -
- - - - - - - - - - - - - - -
- ); -}; - -export default App; diff --git a/frontend/src/components/CopyMenu.tsx b/frontend/src/components/CopyMenu.tsx deleted file mode 100644 index 1f611d062..000000000 --- a/frontend/src/components/CopyMenu.tsx +++ /dev/null @@ -1,115 +0,0 @@ -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; - 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 ( - } - targetClass="blue" - content={() => ( - - - - - servers.get(id)!.server.name} - targetClass="blue" - targetStyle={{ display: "flex", gap: "0.5rem" }} - searchStyle={{ width: "100%" }} - menuClass="scroller" - menuStyle={{ "max-height": "40vh" }} - position="bottom right" - useSearch - /> - - - - copy {p.type} - - - )} - position="center" - /> - } - content={`copy ${p.type}`} - position="bottom center" - /> - ); -}; - -export default CopyMenu; diff --git a/frontend/src/components/Description.tsx b/frontend/src/components/Description.tsx deleted file mode 100644 index 227e37a59..000000000 --- a/frontend/src/components/Description.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Component, createSignal, onMount, Show } from "solid-js"; -import { client, pushNotification } from ".."; -import { useAppState } from "../state/StateProvider"; -import { UpdateTarget } from "../types"; -import { useToggle } from "../util/hooks"; -import Grid from "./shared/layout/Grid"; -import Loading from "./shared/loading/Loading"; -import CenterMenu from "./shared/menu/CenterMenu"; -import TextArea from "./shared/TextArea"; - -const Description: Component<{ - name: string; - target: UpdateTarget; - description?: string; - userCanUpdate: boolean; -}> = (p) => { - const [show, toggleShow] = useToggle(); - const description = () => { - if (p.description) { - let [description] = p.description.split("\n"); - return description; - } else { - return "add a description"; - } - }; - const [width, setWidth] = createSignal(); - onMount(() => { - setWidth(ref!?.clientWidth); - }); - let ref: HTMLDivElement; - return ( - - {width() ? description() : ""} -
- } - content={() => ( - - )} - /> - ); -}; - -const DescriptionMenu: Component<{ - target: UpdateTarget; - description?: string; - userCanUpdate: boolean; - toggleShow: () => void; -}> = (p) => { - const { builds, servers, deployments } = useAppState(); - let ref: HTMLTextAreaElement; - onMount(() => { - ref?.focus(); - }); - const [desc, setDesc] = createSignal(p.description); - const [loading, setLoading] = createSignal(false); - const update_description = () => { - if (!p.userCanUpdate) return; - setLoading(true); - client - .update_description({ target: p.target, description: desc() || "" }) - .then(() => { - if (p.target.type === "Build") { - builds.update({ ...builds.get(p.target.id)!, description: desc() }); - } else if (p.target.type === "Deployment") { - const deployment = deployments.get(p.target.id)!; - deployments.update({ - ...deployment, - deployment: { ...deployment.deployment, description: desc() }, - }); - } else if (p.target.type === "Server") { - const server = servers.get(p.target.id)!; - servers.update({ - ...server, - server: { ...server.server, description: desc() }, - }); - } - p.toggleShow(); - }) - .catch(() => { - pushNotification("bad", "failed to update description"); - p.toggleShow(); - }); - }; - return ( - -