diff --git a/crates/karapace-server/Cargo.toml b/crates/karapace-server/Cargo.toml new file mode 100644 index 0000000..229bac1 --- /dev/null +++ b/crates/karapace-server/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "karapace-server" +description = "Reference HTTP server for the Karapace remote protocol v1" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +tiny_http.workspace = true +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +clap.workspace = true + +[dev-dependencies] +tempfile.workspace = true +ureq.workspace = true +karapace-remote = { path = "../karapace-remote" } +karapace-store = { path = "../karapace-store" } diff --git a/crates/karapace-server/karapace-server.cdx.json b/crates/karapace-server/karapace-server.cdx.json new file mode 100644 index 0000000..00a36e9 --- /dev/null +++ b/crates/karapace-server/karapace-server.cdx.json @@ -0,0 +1,1411 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "version": 1, + "serialNumber": "urn:uuid:50ff2eca-31eb-4ef0-821c-08eff90a138b", + "metadata": { + "timestamp": "2026-02-22T14:03:10.627163643Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.5" + } + ], + "component": { + "type": "application", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-server#0.1.0", + "name": "karapace-server", + "version": "0.1.0", + "description": "Reference HTTP server for the Karapace remote protocol v1", + "scope": "required", + "licenses": [ + { + "expression": "EUPL-1.2" + } + ], + "purl": "pkg:cargo/karapace-server@0.1.0?download_url=file://.", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/marcoallegretti/karapace" + } + ], + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-server#0.1.0 bin-target-0", + "name": "karapace_server", + "version": "0.1.0", + "purl": "pkg:cargo/karapace-server@0.1.0?download_url=file://.#src/lib.rs" + }, + { + "type": "application", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-server#0.1.0 bin-target-1", + "name": "karapace-server", + "version": "0.1.0", + "purl": "pkg:cargo/karapace-server@0.1.0?download_url=file://.#src/main.rs" + } + ] + } + }, + "components": [ + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "name": "aho-corasick", + "version": "1.1.4", + "description": "Fast multiple substring searching.", + "scope": "required", + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/aho-corasick@1.1.4", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/BurntSushi/aho-corasick" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/aho-corasick" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "name": "anstream", + "version": "0.6.21", + "description": "IO stream adapters for writing colored text that will gracefully degrade according to your terminal's capabilities.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstream@0.6.21", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "name": "anstyle-parse", + "version": "0.2.7", + "description": "Parse ANSI Style Escapes", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle-parse@0.2.7", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "name": "anstyle-query", + "version": "1.1.5", + "description": "Look up colored console capabilities", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle-query@1.1.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "name": "anstyle", + "version": "1.0.13", + "description": "ANSI text styling", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle@1.0.13", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ascii@1.1.0", + "name": "ascii", + "version": "1.1.0", + "description": "ASCII-only equivalents to `char`, `str` and `String`.", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/ascii@1.1.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/ascii" + }, + { + "type": "vcs", + "url": "https://github.com/tomprogrammer/rust-ascii" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "name": "cfg-if", + "version": "1.0.4", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#chunked_transfer@1.5.0", + "name": "chunked_transfer", + "version": "1.5.0", + "description": "Encoder and decoder for HTTP chunked transfer coding (RFC 7230 § 4.1)", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/chunked_transfer@1.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/frewsxcv/rust-chunked-transfer" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.60", + "name": "clap", + "version": "4.5.60", + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap@4.5.60", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.60", + "name": "clap_builder", + "version": "4.5.60", + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_builder@4.5.60", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55", + "name": "clap_derive", + "version": "4.5.55", + "description": "Parse command line argument by defining a struct, derive crate.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_derive@4.5.55", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_lex@1.0.0", + "name": "clap_lex", + "version": "1.0.0", + "description": "Minimal, flexible command line parser", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_lex@1.0.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "name": "colorchoice", + "version": "1.0.4", + "description": "Global override of color control", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/colorchoice@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "name": "heck", + "version": "0.5.0", + "description": "heck is a case conversion library.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heck@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/withoutboats/heck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "name": "httpdate", + "version": "1.0.3", + "description": "HTTP date parsing and formatting", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/httpdate@1.0.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/pyfisch/httpdate" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "name": "is_terminal_polyfill", + "version": "1.70.2", + "description": "Polyfill for `is_terminal` stdlib feature for use with older MSRVs", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/is_terminal_polyfill@1.70.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/polyfill-rs/is_terminal_polyfill" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "name": "itoa", + "version": "1.0.17", + "description": "Fast integer primitive to string conversion", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itoa@1.0.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itoa" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/itoa" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "name": "lazy_static", + "version": "1.5.0", + "description": "A macro for declaring lazily evaluated statics in Rust.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lazy_static@1.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/lazy_static" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang-nursery/lazy-static.rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "name": "log", + "version": "0.4.29", + "description": "A lightweight logging facade for Rust ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/log@0.4.29", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/log" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/log" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "name": "matchers", + "version": "0.2.0", + "description": "Regex matching on character and byte streams. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/matchers@0.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/matchers/" + }, + { + "type": "website", + "url": "https://github.com/hawkw/matchers" + }, + { + "type": "vcs", + "url": "https://github.com/hawkw/matchers" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "name": "memchr", + "version": "2.8.0", + "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search. ", + "scope": "required", + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/memchr@2.8.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/memchr/" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/memchr" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/memchr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "name": "nu-ansi-term", + "version": "0.50.3", + "description": "Library for ANSI terminal colors and styles (bold, underline)", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/nu-ansi-term@0.50.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/nushell/nu-ansi-term" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "name": "once_cell", + "version": "1.21.3", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "name": "pin-project-lite", + "version": "0.2.16", + "description": "A lightweight version of pin-project written with declarative macros. ", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/pin-project-lite@0.2.16", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/pin-project-lite" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "name": "proc-macro2", + "version": "1.0.106", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.106", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "name": "quote", + "version": "1.0.44", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.44", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "name": "regex-automata", + "version": "0.4.14", + "description": "Automata construction and matching using regular expressions.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-automata@0.4.14", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-automata" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex/tree/master/regex-automata" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9", + "name": "regex-syntax", + "version": "0.8.9", + "description": "A regular expression parser.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-syntax@0.8.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-syntax" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex/tree/master/regex-syntax" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "name": "serde", + "version": "1.0.228", + "description": "A generic serialization/deserialization framework", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "name": "serde_core", + "version": "1.0.228", + "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_core@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_core" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "name": "serde_derive", + "version": "1.0.228", + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_derive@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://serde.rs/derive.html" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "name": "serde_json", + "version": "1.0.149", + "description": "A JSON serialization file format", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_json@1.0.149", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_json" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/json" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "name": "sharded-slab", + "version": "0.1.7", + "description": "A lock-free concurrent slab. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/sharded-slab@0.1.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sharded-slab/" + }, + { + "type": "website", + "url": "https://github.com/hawkw/sharded-slab" + }, + { + "type": "vcs", + "url": "https://github.com/hawkw/sharded-slab" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "name": "smallvec", + "version": "1.15.1", + "description": "'Small vector' optimization: store up to a small number of items on the stack", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/smallvec@1.15.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/smallvec/" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-smallvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1", + "name": "strsim", + "version": "0.11.1", + "description": "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/strsim@0.11.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/strsim/" + }, + { + "type": "website", + "url": "https://github.com/rapidfuzz/strsim-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rapidfuzz/strsim-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117", + "name": "syn", + "version": "2.0.117", + "description": "Parser for Rust source code", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.117", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "name": "thread_local", + "version": "1.1.9", + "description": "Per-object thread-local storage", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thread_local@1.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/thread_local/" + }, + { + "type": "vcs", + "url": "https://github.com/Amanieu/thread_local-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tiny_http@0.12.0", + "name": "tiny_http", + "version": "0.12.0", + "description": "Low level HTTP server library", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/tiny_http@0.12.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://tiny-http.github.io/tiny-http/tiny_http/index.html" + }, + { + "type": "vcs", + "url": "https://github.com/tiny-http/tiny-http" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "name": "tracing-attributes", + "version": "0.1.31", + "description": "Procedural macro attributes for automatically instrumenting functions. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-attributes@0.1.31", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "name": "tracing-core", + "version": "0.1.36", + "description": "Core primitives for application-level tracing. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-core@0.1.36", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "name": "tracing-log", + "version": "0.2.0", + "description": "Provides compatibility between `tracing` and the `log` crate. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-log@0.2.0", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "name": "tracing-subscriber", + "version": "0.3.22", + "description": "Utilities for implementing and composing `tracing` subscribers. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-subscriber@0.3.22", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "name": "tracing", + "version": "0.1.44", + "description": "Application-level tracing for Rust. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing@0.1.44", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24", + "name": "unicode-ident", + "version": "1.0.24", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-3.0" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.24", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2", + "name": "utf8parse", + "version": "0.2.2", + "description": "Table-driven UTF-8 parser", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/utf8parse@0.2.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/utf8parse/" + }, + { + "type": "vcs", + "url": "https://github.com/alacritty/vte" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21", + "name": "zmij", + "version": "1.0.21", + "description": "A double-to-string conversion algorithm based on Schubfach and yy", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/zmij@1.0.21", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/zmij" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/zmij" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-server#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.60", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#tiny_http@0.12.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ascii@1.1.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#chunked_transfer@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.60", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.60", + "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.60", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "registry+https://github.com/rust-lang/crates.io-index#clap_lex@1.0.0", + "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_lex@1.0.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tiny_http@0.12.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ascii@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#chunked_transfer@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21", + "dependsOn": [] + } + ] +} \ No newline at end of file diff --git a/crates/karapace-server/src/lib.rs b/crates/karapace-server/src/lib.rs new file mode 100644 index 0000000..670102b --- /dev/null +++ b/crates/karapace-server/src/lib.rs @@ -0,0 +1,389 @@ +//! Reference HTTP server library for the Karapace remote protocol v1. +//! +//! Implements the blob store and registry routes defined in `docs/protocol-v1.md`. +//! Storage is file-backed: blobs go into `{data_dir}/blobs/{kind}/{key}`, +//! the registry lives at `{data_dir}/registry.json`. +//! +//! The [`TestServer`] helper starts a server on a random port for integration testing. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; +use tiny_http::{Header, Method, Response, Server, StatusCode}; +use tracing::{debug, error, info}; + +/// In-memory + file-backed blob store. +pub struct Store { + data_dir: PathBuf, + /// Cache of registry data (kept in memory for atomic read-modify-write). + registry: RwLock>>, +} + +impl Store { + pub fn new(data_dir: PathBuf) -> Self { + let reg_path = data_dir.join("registry.json"); + let registry = if reg_path.exists() { + fs::read(®_path).ok() + } else { + None + }; + + Self { + data_dir, + registry: RwLock::new(registry), + } + } + + pub fn data_dir(&self) -> &Path { + &self.data_dir + } + + fn blob_dir(&self, kind: &str) -> PathBuf { + self.data_dir.join("blobs").join(kind) + } + + fn blob_path(&self, kind: &str, key: &str) -> PathBuf { + self.blob_dir(kind).join(key) + } + + pub fn put_blob(&self, kind: &str, key: &str, data: &[u8]) -> std::io::Result<()> { + let dir = self.blob_dir(kind); + fs::create_dir_all(&dir)?; + let path = dir.join(key); + fs::write(&path, data)?; + Ok(()) + } + + pub fn get_blob(&self, kind: &str, key: &str) -> Option> { + let path = self.blob_path(kind, key); + fs::read(&path).ok() + } + + pub fn has_blob(&self, kind: &str, key: &str) -> bool { + self.blob_path(kind, key).exists() + } + + pub fn list_blobs(&self, kind: &str) -> Vec { + let dir = self.blob_dir(kind); + if !dir.exists() { + return Vec::new(); + } + fs::read_dir(dir) + .map(|rd| { + rd.filter_map(Result::ok) + .filter_map(|e| e.file_name().to_str().map(String::from)) + .collect() + }) + .unwrap_or_default() + } + + pub fn put_registry(&self, data: &[u8]) -> std::io::Result<()> { + let reg_path = self.data_dir.join("registry.json"); + fs::create_dir_all(&self.data_dir)?; + fs::write(®_path, data)?; + let mut reg = self.registry.write().expect("registry lock poisoned"); + *reg = Some(data.to_vec()); + Ok(()) + } + + pub fn get_registry(&self) -> Option> { + let reg = self.registry.read().expect("registry lock poisoned"); + reg.clone() + } +} + +/// Valid blob kinds per protocol spec. +pub fn is_valid_kind(kind: &str) -> bool { + matches!(kind, "Object" | "Layer" | "Metadata") +} + +/// Map the HttpBackend's plural lowercase path prefix to the server's internal kind name. +/// `/objects/` → "Object", `/layers/` → "Layer", `/metadata/` → "Metadata". +fn map_client_kind(prefix: &str) -> Option<&'static str> { + match prefix { + "objects" => Some("Object"), + "layers" => Some("Layer"), + "metadata" => Some("Metadata"), + _ => None, + } +} + +/// Parse a URL path into (kind, key). +/// +/// Accepts two URL schemes: +/// - Server-canonical: `/blobs/Object/abc123` +/// - Client (HttpBackend): `/objects/abc123`, `/layers/abc123`, `/metadata/abc123` +pub fn parse_blob_route(path: &str) -> Option<(&str, Option<&str>)> { + // Try /blobs/{Kind}/... first + if let Some(rest) = path.strip_prefix("/blobs/") { + if let Some(idx) = rest.find('/') { + let kind = &rest[..idx]; + let key = &rest[idx + 1..]; + if is_valid_kind(kind) && !key.is_empty() { + return Some((kind, Some(key))); + } + } else if is_valid_kind(rest) { + return Some((rest, None)); + } + } + None +} + +/// Parse the client URL scheme: `/{plural_kind}/{key}` or `/{plural_kind}/`. +fn parse_client_route(path: &str) -> Option<(&'static str, Option<&str>)> { + let path = path.strip_prefix('/')?; + if let Some(idx) = path.find('/') { + let prefix = &path[..idx]; + let rest = &path[idx + 1..]; + let kind = map_client_kind(prefix)?; + if rest.is_empty() { + Some((kind, None)) + } else { + Some((kind, Some(rest))) + } + } else { + let kind = map_client_kind(path)?; + Some((kind, None)) + } +} + +fn respond_err(req: tiny_http::Request, code: u16, msg: &str) { + let _ = req.respond(Response::from_string(msg).with_status_code(StatusCode(code))); +} + +fn respond_octet(req: tiny_http::Request, data: Vec) { + let header = + Header::from_bytes("Content-Type", "application/octet-stream").expect("valid header"); + let _ = req.respond(Response::from_data(data).with_header(header)); +} + +fn respond_json(req: tiny_http::Request, json: impl Into>) { + let header = Header::from_bytes("Content-Type", "application/json").expect("valid header"); + let _ = req.respond(Response::from_data(json.into()).with_header(header)); +} + +fn read_body(req: &mut tiny_http::Request) -> Option> { + let mut body = Vec::new(); + if req.as_reader().read_to_end(&mut body).is_ok() { + Some(body) + } else { + None + } +} + +fn handle_blob_keyed( + store: &Store, + mut req: tiny_http::Request, + method: &Method, + kind: &str, + key: &str, +) { + match *method { + Method::Put => { + let Some(body) = read_body(&mut req) else { + respond_err(req, 500, "read error"); + return; + }; + match store.put_blob(kind, key, &body) { + Ok(()) => { + info!("PUT {kind}/{key}: {} bytes", body.len()); + let _ = req.respond(Response::from_string("ok")); + } + Err(e) => { + error!("PUT {kind}/{key}: {e}"); + respond_err(req, 500, &format!("write error: {e}")); + } + } + } + Method::Get => match store.get_blob(kind, key) { + Some(data) => respond_octet(req, data), + None => respond_err(req, 404, "not found"), + }, + Method::Head => { + let code = if store.has_blob(kind, key) { 200 } else { 404 }; + let _ = req.respond(Response::empty(code)); + } + _ => respond_err(req, 405, "method not allowed"), + } +} + +fn handle_registry(store: &Store, mut req: tiny_http::Request, method: &Method) { + match *method { + Method::Put => { + let Some(body) = read_body(&mut req) else { + respond_err(req, 500, "read error"); + return; + }; + match store.put_registry(&body) { + Ok(()) => { + info!("PUT /registry: {} bytes", body.len()); + let _ = req.respond(Response::from_string("ok")); + } + Err(e) => { + error!("PUT /registry: {e}"); + respond_err(req, 500, &format!("write error: {e}")); + } + } + } + Method::Get => match store.get_registry() { + Some(data) => respond_json(req, data), + None => respond_err(req, 404, "not found"), + }, + _ => respond_err(req, 405, "method not allowed"), + } +} + +/// Handle a single HTTP request, dispatching to the appropriate route handler. +pub fn handle_request(store: &Store, req: tiny_http::Request) { + let method = req.method().clone(); + let url = req.url().to_owned(); + debug!("{method} {url}"); + + // Try both URL schemes: /blobs/Kind/key (server canonical) and /kind_plural/key (client) + let route = parse_blob_route(&url).or_else(|| parse_client_route(&url)); + if let Some(parsed) = route { + match parsed { + (kind, Some(key)) => handle_blob_keyed(store, req, &method, kind, key), + (kind, None) if method == Method::Get => { + let keys = store.list_blobs(kind); + let json = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_owned()); + respond_json(req, json.into_bytes()); + } + _ => respond_err(req, 405, "method not allowed"), + } + } else if url == "/registry" { + handle_registry(store, req, &method); + } else if url == "/health" && method == Method::Get { + let _ = req.respond(Response::from_string(r#"{"status":"ok"}"#)); + } else { + respond_err(req, 404, "not found"); + } +} + +/// Start the server loop, blocking the current thread. +pub fn run_server(store: &Arc, addr: &str) { + let server = Server::http(addr).expect("failed to bind HTTP server"); + for request in server.incoming_requests() { + handle_request(store, request); + } +} + +/// A test helper that starts a karapace-server on a random port in a background thread. +/// +/// The server listens on `127.0.0.1:{port}` and stores data in the provided `data_dir`. +/// Drop the `TestServer` to stop the server (via `Server::unblock`). +pub struct TestServer { + pub url: String, + pub port: u16, + pub data_dir: PathBuf, + _server: Arc, + _handle: std::thread::JoinHandle<()>, +} + +impl TestServer { + /// Start a test server with a temporary data directory. + /// Binds to `127.0.0.1:0` (random port). + pub fn start(data_dir: PathBuf) -> Self { + fs::create_dir_all(&data_dir).expect("failed to create test data dir"); + let server = + Arc::new(Server::http("127.0.0.1:0").expect("failed to bind test HTTP server")); + let port = server.server_addr().to_ip().expect("not an IP addr").port(); + let url = format!("http://127.0.0.1:{port}"); + + let store = Arc::new(Store::new(data_dir.clone())); + let srv = Arc::clone(&server); + let handle = std::thread::spawn(move || { + for request in srv.incoming_requests() { + handle_request(&store, request); + } + }); + + Self { + url, + port, + data_dir, + _server: server, + _handle: handle, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_blob_route_object_with_key() { + let (kind, key) = parse_blob_route("/blobs/Object/abc123").unwrap(); + assert_eq!(kind, "Object"); + assert_eq!(key, Some("abc123")); + } + + #[test] + fn parse_blob_route_layer_list() { + let (kind, key) = parse_blob_route("/blobs/Layer").unwrap(); + assert_eq!(kind, "Layer"); + assert_eq!(key, None); + } + + #[test] + fn parse_blob_route_metadata_with_key() { + let (kind, key) = parse_blob_route("/blobs/Metadata/env_abc").unwrap(); + assert_eq!(kind, "Metadata"); + assert_eq!(key, Some("env_abc")); + } + + #[test] + fn parse_blob_route_invalid_kind() { + assert!(parse_blob_route("/blobs/Invalid/key").is_none()); + } + + #[test] + fn parse_blob_route_missing_prefix() { + assert!(parse_blob_route("/other/Object/key").is_none()); + } + + #[test] + fn store_blob_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let store = Store::new(dir.path().to_path_buf()); + + store.put_blob("Object", "hash1", b"content").unwrap(); + assert!(store.has_blob("Object", "hash1")); + assert_eq!(store.get_blob("Object", "hash1"), Some(b"content".to_vec())); + assert!(!store.has_blob("Object", "missing")); + } + + #[test] + fn store_list_blobs() { + let dir = tempfile::tempdir().unwrap(); + let store = Store::new(dir.path().to_path_buf()); + + store.put_blob("Layer", "l1", b"a").unwrap(); + store.put_blob("Layer", "l2", b"b").unwrap(); + let mut keys = store.list_blobs("Layer"); + keys.sort(); + assert_eq!(keys, vec!["l1", "l2"]); + } + + #[test] + fn store_registry_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let store = Store::new(dir.path().to_path_buf()); + + assert!(store.get_registry().is_none()); + store.put_registry(b"{\"entries\":{}}").unwrap(); + assert_eq!(store.get_registry(), Some(b"{\"entries\":{}}".to_vec())); + } + + #[test] + fn store_registry_persists_to_disk() { + let dir = tempfile::tempdir().unwrap(); + { + let store = Store::new(dir.path().to_path_buf()); + store.put_registry(b"reg_data").unwrap(); + } + let store2 = Store::new(dir.path().to_path_buf()); + assert_eq!(store2.get_registry(), Some(b"reg_data".to_vec())); + } +} diff --git a/crates/karapace-server/src/main.rs b/crates/karapace-server/src/main.rs new file mode 100644 index 0000000..d04f73b --- /dev/null +++ b/crates/karapace-server/src/main.rs @@ -0,0 +1,38 @@ +use clap::Parser; +use karapace_server::Store; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::info; + +#[derive(Parser)] +#[command(name = "karapace-server", about = "Karapace remote protocol v1 server")] +struct Cli { + /// Port to listen on. + #[arg(long, default_value_t = 8321)] + port: u16, + + /// Directory to store blobs and registry data. + #[arg(long, default_value = "./karapace-remote-data")] + data_dir: PathBuf, +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + + fs::create_dir_all(&cli.data_dir).expect("failed to create data directory"); + + let addr = format!("0.0.0.0:{}", cli.port); + info!("starting karapace-server on {addr}"); + info!("data directory: {}", cli.data_dir.display()); + + let store = Arc::new(Store::new(cli.data_dir)); + karapace_server::run_server(&store, &addr); +} diff --git a/crates/karapace-server/tests/http_e2e.rs b/crates/karapace-server/tests/http_e2e.rs new file mode 100644 index 0000000..9b2aa96 --- /dev/null +++ b/crates/karapace-server/tests/http_e2e.rs @@ -0,0 +1,294 @@ +//! IG-M3: HTTP client ↔ server E2E integration tests. +//! +//! These tests start a real `karapace-server` in-process on a random port +//! and exercise the real `HttpBackend` client against it. No mocks. + +use karapace_remote::http::HttpBackend; +use karapace_remote::{BlobKind, RemoteBackend, RemoteConfig}; +use karapace_server::TestServer; +use karapace_store::{ + EnvMetadata, EnvState, LayerKind, LayerManifest, LayerStore, MetadataStore, ObjectStore, + StoreLayout, +}; +fn start_server() -> (TestServer, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let server = TestServer::start(dir.path().to_path_buf()); + (server, dir) +} + +fn make_client(url: &str) -> HttpBackend { + HttpBackend::new(RemoteConfig { + url: url.to_owned(), + auth_token: None, + }) +} + +/// Create a local store with a mock-built environment for push/pull testing. +fn setup_local_env(dir: &std::path::Path) -> (StoreLayout, String) { + let layout = StoreLayout::new(dir); + layout.initialize().unwrap(); + + let obj_store = ObjectStore::new(layout.clone()); + let layer_store = LayerStore::new(layout.clone()); + let meta_store = MetadataStore::new(layout.clone()); + + let obj_hash = obj_store.put(b"test data content").unwrap(); + let manifest_hash = obj_store.put(b"{\"manifest\": \"test\"}").unwrap(); + + let layer = LayerManifest { + hash: "layer_hash_001".to_owned(), + kind: LayerKind::Base, + parent: None, + object_refs: vec![obj_hash], + read_only: true, + tar_hash: String::new(), + }; + let layer_content_hash = layer_store.put(&layer).unwrap(); + + let meta = EnvMetadata { + env_id: "env_abc123".into(), + short_id: "env_abc123".into(), + name: Some("test-env".to_owned()), + state: EnvState::Built, + base_layer: layer_content_hash.into(), + dependency_layers: vec![], + policy_layer: None, + manifest_hash: manifest_hash.into(), + ref_count: 1, + created_at: "2025-01-01T00:00:00Z".to_owned(), + updated_at: "2025-01-01T00:00:00Z".to_owned(), + checksum: None, + }; + meta_store.put(&meta).unwrap(); + + (layout, "env_abc123".to_owned()) +} + +// --- Tests --- + +#[test] +fn http_e2e_blob_roundtrip() { + let (server, _dir) = start_server(); + let client = make_client(&server.url); + + // PUT + client + .put_blob(BlobKind::Object, "hash1", b"hello world") + .unwrap(); + + // GET + let data = client.get_blob(BlobKind::Object, "hash1").unwrap(); + assert_eq!(data, b"hello world"); + + // HEAD — exists + assert!(client.has_blob(BlobKind::Object, "hash1").unwrap()); + + // HEAD — missing + assert!(!client.has_blob(BlobKind::Object, "missing").unwrap()); + + // Multiple kinds + client + .put_blob(BlobKind::Layer, "l1", b"layer-data") + .unwrap(); + client + .put_blob(BlobKind::Metadata, "m1", b"meta-data") + .unwrap(); + assert_eq!( + client.get_blob(BlobKind::Layer, "l1").unwrap(), + b"layer-data" + ); + assert_eq!( + client.get_blob(BlobKind::Metadata, "m1").unwrap(), + b"meta-data" + ); +} + +#[test] +fn http_e2e_push_pull_full_env() { + let (server, _srv_dir) = start_server(); + let client = make_client(&server.url); + + // Set up source store with a mock environment + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + + // Push to real server + let push_result = + karapace_remote::push_env(&src_layout, &env_id, &client, Some("test@latest")).unwrap(); + assert_eq!(push_result.objects_pushed, 2); + assert_eq!(push_result.layers_pushed, 1); + + // Pull into a fresh store + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let pull_result = karapace_remote::pull_env(&dst_layout, &env_id, &client).unwrap(); + assert_eq!(pull_result.objects_pulled, 2); + assert_eq!(pull_result.layers_pulled, 1); + + // Verify metadata identical + let src_meta = MetadataStore::new(src_layout).get(&env_id).unwrap(); + let dst_meta = MetadataStore::new(dst_layout.clone()).get(&env_id).unwrap(); + assert_eq!(src_meta.env_id, dst_meta.env_id); + assert_eq!(src_meta.name, dst_meta.name); + assert_eq!(src_meta.base_layer, dst_meta.base_layer); + assert_eq!(src_meta.manifest_hash, dst_meta.manifest_hash); + + // Verify objects byte-for-byte identical + let src_obj = ObjectStore::new(StoreLayout::new(src_dir.path())); + let dst_obj = ObjectStore::new(dst_layout.clone()); + let src_data = src_obj.get(&src_meta.manifest_hash).unwrap(); + let dst_data = dst_obj.get(&dst_meta.manifest_hash).unwrap(); + assert_eq!(src_data, dst_data); + + // Verify layers identical + let src_layer = LayerStore::new(StoreLayout::new(src_dir.path())) + .get(&src_meta.base_layer) + .unwrap(); + let dst_layer = LayerStore::new(dst_layout) + .get(&dst_meta.base_layer) + .unwrap(); + assert_eq!(src_layer.object_refs, dst_layer.object_refs); + assert_eq!(src_layer.kind, dst_layer.kind); +} + +#[test] +fn http_e2e_registry_roundtrip() { + let (server, _dir) = start_server(); + let client = make_client(&server.url); + + // Set up and push an environment with a tag + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + karapace_remote::push_env(&src_layout, &env_id, &client, Some("myapp@latest")).unwrap(); + + // Resolve the reference + let resolved = karapace_remote::resolve_ref(&client, "myapp@latest").unwrap(); + assert_eq!(resolved, env_id); +} + +#[test] +fn http_e2e_concurrent_4_clients() { + let (server, _dir) = start_server(); + let url = server.url.clone(); + + let handles: Vec<_> = (0..4) + .map(|thread_idx| { + let u = url.clone(); + std::thread::spawn(move || { + let client = make_client(&u); + for i in 0..10 { + let key = format!("t{thread_idx}_blob_{i}"); + let data = format!("data-{thread_idx}-{i}"); + client + .put_blob(BlobKind::Object, &key, data.as_bytes()) + .unwrap(); + } + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + + // Verify all 40 blobs exist + let client = make_client(&server.url); + for thread_idx in 0..4 { + for i in 0..10 { + let key = format!("t{thread_idx}_blob_{i}"); + let expected = format!("data-{thread_idx}-{i}"); + let data = client.get_blob(BlobKind::Object, &key).unwrap(); + assert_eq!(data, expected.as_bytes(), "blob {key} data mismatch"); + } + } +} + +#[test] +fn http_e2e_server_restart_persistence() { + let data_dir = tempfile::tempdir().unwrap(); + + // Start server, push data + { + let server = TestServer::start(data_dir.path().to_path_buf()); + let client = make_client(&server.url); + + client + .put_blob(BlobKind::Object, "persist1", b"data1") + .unwrap(); + client + .put_blob(BlobKind::Layer, "persist2", b"data2") + .unwrap(); + client.put_registry(b"{\"entries\":{}}").unwrap(); + // server drops here — stops listening + } + + // Start new server on same data_dir + { + let server2 = TestServer::start(data_dir.path().to_path_buf()); + let client2 = make_client(&server2.url); + + // All data must survive + assert_eq!( + client2.get_blob(BlobKind::Object, "persist1").unwrap(), + b"data1" + ); + assert_eq!( + client2.get_blob(BlobKind::Layer, "persist2").unwrap(), + b"data2" + ); + assert_eq!(client2.get_registry().unwrap(), b"{\"entries\":{}}"); + } +} + +#[test] +fn http_e2e_integrity_on_pull() { + let (server, server_data) = start_server(); + let client = make_client(&server.url); + + // Push a real environment + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + karapace_remote::push_env(&src_layout, &env_id, &client, None).unwrap(); + + // Tamper with an object on the server's filesystem directly + let src_meta = MetadataStore::new(src_layout).get(&env_id).unwrap(); + let manifest_hash = src_meta.manifest_hash.to_string(); + let tampered_path = server_data + .path() + .join("blobs") + .join("Object") + .join(&manifest_hash); + std::fs::write(&tampered_path, b"CORRUPTED DATA").unwrap(); + + // Pull into a fresh store — must detect integrity failure + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let result = karapace_remote::pull_env(&dst_layout, &env_id, &client); + assert!( + result.is_err(), + "pull must fail when a blob has been tampered with" + ); + let err_msg = format!("{}", result.unwrap_err()); + assert!( + err_msg.contains("integrity") || err_msg.contains("Integrity"), + "error must mention integrity failure, got: {err_msg}" + ); +} + +#[test] +fn http_e2e_404_on_missing() { + let (server, _dir) = start_server(); + let client = make_client(&server.url); + + let result = client.get_blob(BlobKind::Object, "nonexistent"); + assert!(result.is_err(), "GET missing blob must return error"); + let err_msg = format!("{}", result.unwrap_err()); + assert!( + err_msg.contains("404") || err_msg.contains("not found") || err_msg.contains("Not Found"), + "error must indicate 404, got: {err_msg}" + ); +}