diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7a9495a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,723 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +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-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tripoli-edge" +version = "0.1.0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..adc4383 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["tripoli-edge"] diff --git a/README.md b/README.md index 3708339..837d9d0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ tenté, non simulé. | SSH public | Toujours ouvert (choix explicite, pas encore restreint au VPN) | | Ansible (rôle de base) | Fait — `ansible/` | | OpenTofu (serveur + DNS) | Code écrit — `tofu/` ; serveur en donnée en lecture seule (jamais recréé/détruit), DNS géré via le provider OVH. `plan`/`apply` pas encore exécutés (nécessitent des identifiants OVH/Hetzner non fournis) | -| `tripoli-edge` (Rust, Phase 2) | Pas commencé | +| `tripoli-edge` (Rust, Phase 2) | Fait et déployé en prod — `tripoli-edge/`, service systemd, derrière Caddy, vérifié via `https://tripoli.cloud/healthz` et `/metrics` | | `tripoli-api` (Rust, Phase 3) | Pas commencé | | `tripoli-netctl` (Rust, Phase 4) | Pas commencé | | Observabilité dédiée (Phase 5) | Pas commencé (une stack Prometheus/Grafana existe déjà sur le serveur, partagée avec les autres projets) | diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml index ec4ed7e..ca8f517 100644 --- a/ansible/roles/base/tasks/main.yml +++ b/ansible/roles/base/tasks/main.yml @@ -34,6 +34,19 @@ proto: udp comment: "tripoli-cloud : WireGuard admin" +- name: Autoriser Caddy à joindre tripoli-edge (172.17.0.1:9000) + # Même contrainte que la règle 8090/tcp de voltvision déjà en place : + # une requête du conteneur Caddy (réseau "edge", 172.19.0.0/16) vers + # 172.17.0.1 (passerelle docker0) passe par la chaîne INPUT de l'hôte, + # pas FORWARD - ufw la bloque par défaut même si tripoli-edge écoute + # bien sur cette adresse. + community.general.ufw: + rule: allow + from_ip: "172.16.0.0/12" + port: "9000" + proto: tcp + comment: "tripoli-cloud : Caddy -> tripoli-edge" + - name: SSH restreint au tunnel WireGuard uniquement when: ssh_restrict_to_wireguard block: diff --git a/tripoli-edge/Cargo.toml b/tripoli-edge/Cargo.toml new file mode 100644 index 0000000..e24cad4 --- /dev/null +++ b/tripoli-edge/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tripoli-edge" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "time", "signal"] } +hyper = { version = "1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +http-body-util = "0.1" +bytes = "1" +tokio-util = "0.7" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } diff --git a/tripoli-edge/README.md b/tripoli-edge/README.md new file mode 100644 index 0000000..582622e --- /dev/null +++ b/tripoli-edge/README.md @@ -0,0 +1,60 @@ +# tripoli-edge + +Service Rust interne placé derrière le Caddy déjà en place sur le +serveur (voir `README.md` à la racine du dépôt pour le pourquoi : ce +Caddy tient déjà les ports 80/443 pour six autres domaines). + +Ce que fait tripoli-edge, concrètement : + +- `GET /healthz` : répond directement, sans toucher à tripoli-api. +- `GET /metrics` : métriques au format d'exposition Prometheus + (compteurs à la main, voir `src/metrics.rs`). +- Tout le reste : reverse-proxy vers `tripoli-api` (méthode, en-têtes et + corps conservés). Tant que `tripoli-api` n'existe pas (Phase 3), ça + répond un vrai `502 Bad Gateway`, pas une réponse simulée. +- Logs structurés JSON (`tracing` + `tracing-subscriber`) pour chaque + requête : méthode, chemin, host, statut, latence. + +## Configuration (variables d'environnement) + +| Variable | Défaut | Rôle | +|---|---|---| +| `TRIPOLI_EDGE_LISTEN` | `127.0.0.1:9000,172.17.0.1:9000` | Adresses d'écoute (jamais `0.0.0.0` — voir plus bas) | +| `TRIPOLI_API_UPSTREAM` | `127.0.0.1:8081` | Adresse de tripoli-api | +| `RUST_LOG` | — | Niveau de logs (`info`, `debug`, ...) | + +`172.17.0.1` est la passerelle `docker0` du serveur : c'est ce qui permet +au conteneur Caddy (sur un réseau Docker différent) de joindre +tripoli-edge, qui tourne nativement sur l'hôte — même pattern que le +Nginx de VoltVision sur ce même serveur. tripoli-edge n'écoute jamais sur +l'interface publique. + +## Déploiement (fait manuellement pour cette phase) + +L'automatisation complète (build + déploiement via CI) est prévue dans +la section CI/CD du brief, pas encore faite. Pour l'instant, sur le +serveur : + +```bash +# build (rustc/cargo installés via apt) +cd /opt/tripoli-edge/src && cargo build --release +install -m 755 target/release/tripoli-edge /usr/local/bin/tripoli-edge +systemctl restart tripoli-edge +``` + +Le service tourne sous l'utilisateur système `tripoli` (sans shell, +sans privilège) via `deploy/tripoli-edge.service`, déjà installé dans +`/etc/systemd/system/`. + +Le bloc Caddy est dans `deploy/Caddyfile.snippet` — ajouté à +`/opt/edge/Caddyfile` sur le serveur (fichier partagé, donc pas commité +dans son intégralité ici). Une règle `ufw` supplémentaire était +nécessaire pour que Caddy puisse joindre `172.17.0.1:9000` (voir +`ansible/roles/base/tasks/main.yml`). + +## Vérifié en production + +``` +curl https://tripoli.cloud/healthz -> 200 "ok" +curl https://tripoli.cloud/metrics -> métriques Prometheus +``` diff --git a/tripoli-edge/deploy/Caddyfile.snippet b/tripoli-edge/deploy/Caddyfile.snippet new file mode 100644 index 0000000..55b8927 --- /dev/null +++ b/tripoli-edge/deploy/Caddyfile.snippet @@ -0,0 +1,25 @@ +# Bloc ajouté à /opt/edge/Caddyfile sur le serveur (fichier partagé avec +# les 6 autres projets hébergés, donc pas dans ce dépôt en entier — voir +# le README de ce dossier). +# +# Pourquoi Caddy et pas tripoli-edge lui-même sur 80/443 : les ports 80 et +# 443 appartiennent déjà à ce Caddy. tripoli-edge est un service interne +# (172.17.0.1:9000, passerelle docker0, jamais exposé publiquement) : +# Caddy fait TLS/ACME et le routage par Host, tripoli-edge fait le reste. + +tripoli.cloud, www.tripoli.cloud { + reverse_proxy 172.17.0.1:9000 + + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + -Server + } + + log { + output file /var/log/caddy/tripoli-cloud.log + format json + } +} diff --git a/tripoli-edge/deploy/tripoli-edge.service b/tripoli-edge/deploy/tripoli-edge.service new file mode 100644 index 0000000..2f8c506 --- /dev/null +++ b/tripoli-edge/deploy/tripoli-edge.service @@ -0,0 +1,25 @@ +[Unit] +Description=tripoli-edge - service interne derrière Caddy pour tripoli.cloud +After=network.target + +[Service] +Type=simple +User=tripoli +Group=tripoli +ExecStart=/usr/local/bin/tripoli-edge +Restart=on-failure +RestartSec=2 +Environment=RUST_LOG=info +Environment=TRIPOLI_EDGE_LISTEN=127.0.0.1:9000,172.17.0.1:9000 +Environment=TRIPOLI_API_UPSTREAM=127.0.0.1:8081 + +# Un utilisateur système dédié, sans shell, plutôt que root : ce service +# n'a besoin d'aucun privilège (pas de port <1024, pas d'accès disque +# hors ses propres logs via journald). +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/tripoli-edge/src/config.rs b/tripoli-edge/src/config.rs new file mode 100644 index 0000000..2538e9f --- /dev/null +++ b/tripoli-edge/src/config.rs @@ -0,0 +1,42 @@ +use std::net::SocketAddr; + +/// Configuration minimale, entièrement par variables d'environnement : +/// pas de fichier de config à ce stade, ce serait de la complexité pour +/// rien tant qu'il n'y a qu'un seul déploiement (derrière Caddy, sur ce +/// même serveur). +pub struct Config { + /// Adresses d'écoute internes. Toujours la boucle locale et/ou une + /// passerelle docker (jamais 0.0.0.0) : c'est le même choix que pour + /// le Nginx de VoltVision sur ce serveur (172.17.0.1, passerelle + /// docker0, atteignable depuis le conteneur Caddy). Une valeur reste + /// une adresse unique ; plusieurs sont séparées par une virgule. + pub listen_addrs: Vec, + /// Adresse de tripoli-api. Tant que ce service n'existe pas (Phase 3), + /// les requêtes proxyées échoueront en 502 - un vrai 502, pas une + /// réponse simulée. + pub upstream_addr: SocketAddr, +} + +impl Config { + pub fn from_env() -> Self { + let listen_addrs = std::env::var("TRIPOLI_EDGE_LISTEN") + .unwrap_or_else(|_| "127.0.0.1:9000,172.17.0.1:9000".to_string()) + .split(',') + .map(|addr| { + addr.trim() + .parse() + .expect("TRIPOLI_EDGE_LISTEN doit être une liste d'adresses host:port valides") + }) + .collect(); + + let upstream_addr = std::env::var("TRIPOLI_API_UPSTREAM") + .unwrap_or_else(|_| "127.0.0.1:8081".to_string()) + .parse() + .expect("TRIPOLI_API_UPSTREAM doit être une adresse host:port valide"); + + Self { + listen_addrs, + upstream_addr, + } + } +} diff --git a/tripoli-edge/src/main.rs b/tripoli-edge/src/main.rs new file mode 100644 index 0000000..5b9bb01 --- /dev/null +++ b/tripoli-edge/src/main.rs @@ -0,0 +1,106 @@ +mod config; +mod metrics; +mod proxy; + +use std::net::SocketAddr; +use std::sync::Arc; + +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; + +use config::Config; +use metrics::Metrics; +use proxy::UpstreamClient; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .json() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let config = Config::from_env(); + let metrics = Arc::new(Metrics::default()); + let client = Arc::new(proxy::build_client()); + let shutdown = CancellationToken::new(); + + let mut listeners = Vec::new(); + for addr in &config.listen_addrs { + let listener = TcpListener::bind(addr) + .await + .unwrap_or_else(|err| panic!("impossible d'écouter sur {addr}: {err}")); + listeners.push(listener); + } + + tracing::info!( + listen_addrs = ?config.listen_addrs, + upstream_addr = %config.upstream_addr, + "tripoli-edge démarré" + ); + + let mut tasks = Vec::new(); + for listener in listeners { + let client = Arc::clone(&client); + let metrics = Arc::clone(&metrics); + let shutdown = shutdown.clone(); + let upstream_addr = config.upstream_addr; + tasks.push(tokio::spawn(accept_loop( + listener, + client, + metrics, + upstream_addr, + shutdown, + ))); + } + + tokio::signal::ctrl_c() + .await + .expect("impossible d'installer le gestionnaire de signal"); + tracing::info!("signal d'arrêt reçu, extinction de tripoli-edge"); + shutdown.cancel(); + + for task in tasks { + let _ = task.await; + } +} + +async fn accept_loop( + listener: TcpListener, + client: Arc, + metrics: Arc, + upstream_addr: SocketAddr, + shutdown: CancellationToken, +) { + loop { + let (stream, peer_addr) = tokio::select! { + accepted = listener.accept() => match accepted { + Ok(pair) => pair, + Err(err) => { + tracing::warn!(error = %err, "échec d'acceptation d'une connexion"); + continue; + } + }, + _ = shutdown.cancelled() => return, + }; + + let client = Arc::clone(&client); + let metrics = Arc::clone(&metrics); + + tokio::spawn(async move { + let io = TokioIo::new(stream); + let service = service_fn(move |req| { + proxy::handle(req, Arc::clone(&client), upstream_addr, Arc::clone(&metrics)) + }); + + if let Err(err) = http1::Builder::new() + .serve_connection(io, service) + .await + { + tracing::debug!(peer = %peer_addr, error = %err, "connexion terminée en erreur"); + } + }); + } +} diff --git a/tripoli-edge/src/metrics.rs b/tripoli-edge/src/metrics.rs new file mode 100644 index 0000000..4e358e0 --- /dev/null +++ b/tripoli-edge/src/metrics.rs @@ -0,0 +1,84 @@ +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; + +/// Compteurs à la main plutôt qu'une crate de métriques : pour cinq +/// compteurs, une lib dédiée serait une abstraction de plus à défendre +/// sans bénéfice réel. `Ordering::Relaxed` suffit ici - on n'a besoin +/// d'aucune synchronisation entre compteurs, juste que chaque incrément +/// soit atomique. +#[derive(Default)] +pub struct Metrics { + requests_total: AtomicU64, + responses_1xx: AtomicU64, + responses_2xx: AtomicU64, + responses_3xx: AtomicU64, + responses_4xx: AtomicU64, + responses_5xx: AtomicU64, + in_flight: AtomicI64, + duration_ms_sum: AtomicU64, +} + +pub struct InFlightGuard<'a>(&'a Metrics); + +impl<'a> InFlightGuard<'a> { + pub fn enter(metrics: &'a Metrics) -> Self { + metrics.in_flight.fetch_add(1, Ordering::Relaxed); + Self(metrics) + } +} + +impl Drop for InFlightGuard<'_> { + fn drop(&mut self) { + self.0.in_flight.fetch_sub(1, Ordering::Relaxed); + } +} + +impl Metrics { + pub fn record(&self, status: u16, duration_ms: u64) { + self.requests_total.fetch_add(1, Ordering::Relaxed); + self.duration_ms_sum.fetch_add(duration_ms, Ordering::Relaxed); + let bucket = match status { + 100..=199 => &self.responses_1xx, + 200..=299 => &self.responses_2xx, + 300..=399 => &self.responses_3xx, + 400..=499 => &self.responses_4xx, + _ => &self.responses_5xx, + }; + bucket.fetch_add(1, Ordering::Relaxed); + } + + /// Format texte Prometheus (exposition format), à la main : pas de + /// registre global, pas de types de métriques (counter/gauge) au-delà + /// de ce qui est vraiment utile ici. + pub fn render(&self) -> String { + let total = self.requests_total.load(Ordering::Relaxed); + let sum_ms = self.duration_ms_sum.load(Ordering::Relaxed); + let avg_ms = if total > 0 { sum_ms as f64 / total as f64 } else { 0.0 }; + + format!( + "# HELP tripoli_edge_requests_total Nombre total de requêtes traitées.\n\ + # TYPE tripoli_edge_requests_total counter\n\ + tripoli_edge_requests_total {total}\n\ + # HELP tripoli_edge_responses_total Réponses par classe de statut HTTP.\n\ + # TYPE tripoli_edge_responses_total counter\n\ + tripoli_edge_responses_total{{class=\"1xx\"}} {c1}\n\ + tripoli_edge_responses_total{{class=\"2xx\"}} {c2}\n\ + tripoli_edge_responses_total{{class=\"3xx\"}} {c3}\n\ + tripoli_edge_responses_total{{class=\"4xx\"}} {c4}\n\ + tripoli_edge_responses_total{{class=\"5xx\"}} {c5}\n\ + # HELP tripoli_edge_in_flight_requests Requêtes en cours de traitement.\n\ + # TYPE tripoli_edge_in_flight_requests gauge\n\ + tripoli_edge_in_flight_requests {in_flight}\n\ + # HELP tripoli_edge_request_duration_ms_avg Latence moyenne, en millisecondes.\n\ + # TYPE tripoli_edge_request_duration_ms_avg gauge\n\ + tripoli_edge_request_duration_ms_avg {avg_ms}\n", + total = total, + c1 = self.responses_1xx.load(Ordering::Relaxed), + c2 = self.responses_2xx.load(Ordering::Relaxed), + c3 = self.responses_3xx.load(Ordering::Relaxed), + c4 = self.responses_4xx.load(Ordering::Relaxed), + c5 = self.responses_5xx.load(Ordering::Relaxed), + in_flight = self.in_flight.load(Ordering::Relaxed), + avg_ms = avg_ms, + ) + } +} diff --git a/tripoli-edge/src/proxy.rs b/tripoli-edge/src/proxy.rs new file mode 100644 index 0000000..2b7068e --- /dev/null +++ b/tripoli-edge/src/proxy.rs @@ -0,0 +1,123 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Instant; + +use bytes::Bytes; +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, Full}; +use hyper::body::Incoming; +use hyper::{Request, Response, StatusCode, Uri}; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::client::legacy::Client; +use hyper_util::rt::TokioExecutor; + +use crate::metrics::{InFlightGuard, Metrics}; + +type BoxError = Box; +pub type EdgeBody = BoxBody; +pub type UpstreamClient = Client; + +pub fn build_client() -> UpstreamClient { + Client::builder(TokioExecutor::new()).build(HttpConnector::new()) +} + +fn text_body(text: impl Into) -> EdgeBody { + Full::new(text.into()) + .map_err(|never| match never {}) + .boxed() +} + +fn text_response(status: StatusCode, body: impl Into) -> Response { + Response::builder() + .status(status) + .header("content-type", "text/plain; charset=utf-8") + .body(text_body(body)) + .expect("réponse texte statique toujours valide") +} + +/// Point d'entrée de chaque requête reçue de Caddy. Deux routes sont +/// gérées par tripoli-edge lui-même (`/healthz`, `/metrics`) ; tout le +/// reste est transmis tel quel à tripoli-api. +pub async fn handle( + req: Request, + client: Arc, + upstream_addr: SocketAddr, + metrics: Arc, +) -> Result, hyper::Error> { + let start = Instant::now(); + let _in_flight = InFlightGuard::enter(&metrics); + + let method = req.method().clone(); + let path = req.uri().path().to_string(); + let host_header = req + .headers() + .get("host") + .and_then(|h| h.to_str().ok()) + .unwrap_or("-") + .to_string(); + + let response = match path.as_str() { + "/healthz" => text_response(StatusCode::OK, "ok"), + "/metrics" => text_response(StatusCode::OK, metrics.render()), + _ => proxy_to_upstream(req, &client, upstream_addr).await, + }; + + let status = response.status().as_u16(); + let duration_ms = start.elapsed().as_millis() as u64; + metrics.record(status, duration_ms); + + tracing::info!( + method = %method, + path = %path, + host = %host_header, + status = status, + duration_ms = duration_ms, + "requête traitée" + ); + + Ok(response) +} + +/// Reconstruit la requête vers tripoli-api en conservant méthode, +/// en-têtes et corps ; seule l'autorité de l'URI change, pour pointer +/// vers l'adresse interne au lieu de `tripoli.cloud`. +async fn proxy_to_upstream( + req: Request, + client: &UpstreamClient, + upstream_addr: SocketAddr, +) -> Response { + let (parts, body) = req.into_parts(); + + let path_and_query = parts + .uri + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + let upstream_uri: Uri = match format!("http://{upstream_addr}{path_and_query}").parse() { + Ok(uri) => uri, + Err(_) => return text_response(StatusCode::BAD_GATEWAY, "URI amont invalide"), + }; + + let mut upstream_req = Request::builder() + .method(parts.method) + .uri(upstream_uri) + .body(body) + .expect("requête amont toujours valide : mêmes en-têtes/corps que l'entrante"); + *upstream_req.headers_mut() = parts.headers; + + match client.request(upstream_req).await { + Ok(resp) => { + let (parts, body) = resp.into_parts(); + let boxed_body = body.map_err(|e| Box::new(e) as BoxError).boxed(); + Response::from_parts(parts, boxed_body) + } + Err(err) => { + tracing::warn!(error = %err, "tripoli-api injoignable"); + text_response( + StatusCode::BAD_GATEWAY, + format!("tripoli-api injoignable : {err}"), + ) + } + } +}