Compare commits
17 commits
bug/layout
...
main
Author | SHA1 | Date | |
---|---|---|---|
8a5bbc3b0b | |||
97f245f218 | |||
8129a98291 | |||
54e02b4c3b | |||
f6f8b7f1ad | |||
e312ba977e | |||
2465163e39 | |||
84613e66a2 | |||
c8b30ebc79 | |||
d7decda3f4 | |||
cd13ea461b | |||
5d19f3d2d7 | |||
084dcdbd3a | |||
3baa841d6f | |||
dd407e7041 | |||
af261e1789 | |||
4ae03aa774 |
24
.drone.yml
|
@ -46,10 +46,12 @@ steps:
|
|||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --option log-lines 100 --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: unit tests
|
||||
- name: unit + func tests
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
GARAGE_TEST_INTEGRATION_EXE: result/bin/garage
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
|
@ -59,15 +61,17 @@ steps:
|
|||
- |
|
||||
nix-build \
|
||||
--no-build-output \
|
||||
--option log-lines 100 \
|
||||
--argstr target x86_64-unknown-linux-musl \
|
||||
--argstr compileMode test
|
||||
- ./result*/bin/garage_api*
|
||||
- ./result*/bin/garage_model*
|
||||
- ./result*/bin/garage_rpc*
|
||||
- ./result*/bin/garage_table*
|
||||
- ./result*/bin/garage_util*
|
||||
- ./result*/bin/garage_web*
|
||||
- ./result*/bin/garage*
|
||||
- ./result/bin/garage_api-*
|
||||
- ./result/bin/garage_model-*
|
||||
- ./result/bin/garage_rpc-*
|
||||
- ./result/bin/garage_table-*
|
||||
- ./result/bin/garage_util-*
|
||||
- ./result/bin/garage_web-*
|
||||
- ./result/bin/garage-*
|
||||
- ./result/bin/integration-*
|
||||
|
||||
- name: smoke-test
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
|
@ -469,6 +473,6 @@ node:
|
|||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 928ea1bb59f3ac19b5ddd2a184f17b7c728cc355877c34e61b3d1b421544d4c3
|
||||
hmac: 3fc19d6f9a3555519c8405e3281b2e74289bb802f644740d5481d53df3a01fa4
|
||||
|
||||
...
|
||||
|
|
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
698
Cargo.lock
generated
|
@ -51,6 +51,212 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "aws-endpoint"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06d059b181b25940b751e8efecc173ceb4fe65f45d8975f56b02e98db5c42fd6"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-types",
|
||||
"http",
|
||||
"regex",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-http"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3049066e3282c98bbf01e90459a1772ccf6c0b96cd1483c3dd5aa34bef9b9de1"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"http",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d70be50ac07c3c2b5f37056271856ac00190e80c19c76c58bcbee5be0b63ec9"
|
||||
dependencies = [
|
||||
"aws-endpoint",
|
||||
"aws-http",
|
||||
"aws-sig-auth",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-xml",
|
||||
"aws-types",
|
||||
"bytes 1.1.0",
|
||||
"http",
|
||||
"md5",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sig-auth"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4012b5192350b5403aba19a01a5a3b1768158dab936c4269d89760970d4812bc"
|
||||
dependencies = [
|
||||
"aws-sigv4",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-types",
|
||||
"http",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f4b9c0c3a34e5152a0cd5e43b8f2cfd780e3bd7a245948d8787e051095ac4c"
|
||||
dependencies = [
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"bytes 1.1.0",
|
||||
"form_urlencoded",
|
||||
"hex",
|
||||
"http",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"ring",
|
||||
"time 0.3.7",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b69dad0aefb1b64e63e0d3a1310dc50191608d8c9e226f2f241f344a7173642e"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-client"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e47a8aca2194672518d6630936507d3b54598c482f13ffe53f9b7932724bbb"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-types",
|
||||
"bytes 1.1.0",
|
||||
"fastrand",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"lazy_static",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98bcfcb063d29c7cc7bb0a64830afe606090de75533c10a11a05460d814e8d9"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes 1.1.0",
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c8bbe92ecdc4e39a612359b09994c45d000591d4951aa7343443f44b47e6696"
|
||||
dependencies = [
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-types",
|
||||
"bytes 1.1.0",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-tower"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f23fdf1253855af3bb4abb25e42ad3152a71241af89014eebf27c14c7a59b81d"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"bytes 1.1.0",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cde96306a54777ec8781aa510830e242de614aa5746274713f5ecac0779f644f"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"num-integer",
|
||||
"ryu",
|
||||
"time 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b0466594a86074a6e96b11284f9a9ddc90c5c5b7d6144ab357a90be49d28c4"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433fd128ea727e9b83b34c72c6d4db1b900f067760fa27b387694fe896633142"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
"rustc_version",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
|
@ -83,6 +289,12 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
|
@ -101,6 +313,16 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e314712951c43123e5920a446464929adc667a5eade7f8fb3997776c9df6e54"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.71"
|
||||
|
@ -116,6 +338,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
|
@ -125,7 +353,7 @@ dependencies = [
|
|||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -140,6 +368,22 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.1"
|
||||
|
@ -201,6 +445,15 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-logs"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8"
|
||||
dependencies = [
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
|
@ -210,6 +463,21 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
|
@ -251,6 +519,15 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
@ -385,6 +662,7 @@ name = "garage"
|
|||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-sdk-s3",
|
||||
"bytes 1.1.0",
|
||||
"futures",
|
||||
"futures-util",
|
||||
|
@ -396,6 +674,7 @@ dependencies = [
|
|||
"garage_web",
|
||||
"git-version",
|
||||
"hex",
|
||||
"http",
|
||||
"kuska-sodiumoxide",
|
||||
"log",
|
||||
"netapp",
|
||||
|
@ -405,6 +684,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_bytes",
|
||||
"sled",
|
||||
"static_init",
|
||||
"structopt",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
@ -419,6 +699,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"crypto-mac 0.10.1",
|
||||
"err-derive 0.3.0",
|
||||
"form_urlencoded",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_model 0.6.0",
|
||||
|
@ -433,6 +714,7 @@ dependencies = [
|
|||
"idna",
|
||||
"log",
|
||||
"md-5",
|
||||
"multer",
|
||||
"nom",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
|
@ -440,6 +722,7 @@ dependencies = [
|
|||
"roxmltree",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"url",
|
||||
|
@ -708,6 +991,31 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
|
@ -760,14 +1068,14 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
|
|||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"fnv",
|
||||
"itoa",
|
||||
"itoa 0.4.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
|
||||
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"http",
|
||||
|
@ -817,11 +1125,12 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate 1.0.1",
|
||||
"itoa",
|
||||
"itoa 0.4.8",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
|
@ -830,6 +1139,23 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64"
|
||||
dependencies = [
|
||||
"ct-logs",
|
||||
"futures-util",
|
||||
"hyper",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
|
@ -841,6 +1167,16 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.11"
|
||||
|
@ -862,6 +1198,12 @@ version = "0.4.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.24"
|
||||
|
@ -871,6 +1213,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuska-handshake"
|
||||
version = "0.2.0"
|
||||
|
@ -903,9 +1254,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.103"
|
||||
version = "0.2.115"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||
checksum = "0a8d982fa7a96a000f6ec4cfe966de9703eccde29750df2bb8949da91b0e818d"
|
||||
|
||||
[[package]]
|
||||
name = "libsodium-sys"
|
||||
|
@ -954,6 +1305,12 @@ dependencies = [
|
|||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
|
@ -969,6 +1326,12 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
@ -997,6 +1360,24 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin 0.9.2",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netapp"
|
||||
version = "0.3.0"
|
||||
|
@ -1068,6 +1449,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.8.0"
|
||||
|
@ -1080,6 +1470,12 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
|
@ -1301,6 +1697,21 @@ version = "0.6.25"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin 0.5.2",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.10"
|
||||
|
@ -1342,6 +1753,40 @@ dependencies = [
|
|||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"ring",
|
||||
"sct",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.5"
|
||||
|
@ -1363,12 +1808,61 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.130"
|
||||
|
@ -1404,7 +1898,7 @@ version = "1.0.68"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"itoa 0.4.8",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
@ -1469,6 +1963,46 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5"
|
||||
|
||||
[[package]]
|
||||
name = "static_init"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "208e44bfab7faad5dee24112ea8af2f76aa0d501ea3370b5d4b81729a528f119"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"parking_lot_core",
|
||||
"static_init_macro",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_init_macro"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a2595fc3aa78f2d0e45dd425b22282dd863273761cc77780914b2cf3003acf"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.23"
|
||||
|
@ -1570,6 +2104,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num_threads",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.5.0"
|
||||
|
@ -1616,10 +2160,21 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.7"
|
||||
name = "tokio-rustls"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
|
||||
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
|
@ -1650,6 +2205,28 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.1"
|
||||
|
@ -1663,10 +2240,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.21"
|
||||
|
@ -1721,6 +2311,12 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.2"
|
||||
|
@ -1766,6 +2362,80 @@ version = "0.10.2+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -1809,6 +2479,12 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575e15bedf6e57b5c2d763ffc6c3c760143466cbd09d762d539680ab5992ded"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4062c749be08d90be727e9c5895371c3a0e49b90ba2b9592dc7afda95cc2b719"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.9.0+zstd.1.5.0"
|
||||
|
|
129
default.nix
|
@ -11,76 +11,115 @@ with import ./nix/common.nix;
|
|||
let
|
||||
crossSystem = { config = target; };
|
||||
in let
|
||||
log = v: builtins.trace v v;
|
||||
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
|
||||
/*
|
||||
The following complexity should be abstracted by makePackageSet' (note the final quote).
|
||||
However its code uses deprecated features of rust-overlay that can lead to bug.
|
||||
Instead, we build our own rustChannel object with the recommended API of rust-overlay.
|
||||
*/
|
||||
rustChannel = pkgs.rustPlatform.rust;
|
||||
|
||||
overrides = pkgs.buildPackages.rustBuilder.overrides.all ++ [
|
||||
/*
|
||||
Rust and Nix triples are not the same. Cargo2nix has a dedicated library
|
||||
to convert Nix triples to Rust ones. We need this conversion as we want to
|
||||
set later options linked to our (rust) target in a generic way. Not only
|
||||
the triple terminology is different, but also the "roles" are named differently.
|
||||
Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target".
|
||||
*/
|
||||
rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform);
|
||||
|
||||
/*
|
||||
Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases.
|
||||
We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0.
|
||||
rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage.
|
||||
So we want a Rust release that is bound to our Nix repository to avoid these problems.
|
||||
See here for more info: https://musl.libc.org/time64.html
|
||||
Because Cargo2nix does not support the Rust environment shipped by NixOS,
|
||||
we emulate the structure of the Rust object created by rustOverlay.
|
||||
In practise, rustOverlay ships rustc+cargo in a single derivation while
|
||||
NixOS ships them in separate ones. We reunite them with symlinkJoin.
|
||||
*/
|
||||
rustChannel = pkgs.symlinkJoin {
|
||||
name ="rust-channel";
|
||||
paths = [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
];
|
||||
};
|
||||
|
||||
overrides = pkgs.rustBuilder.overrides.all ++ [
|
||||
/*
|
||||
We want to inject the git version while keeping the build deterministic.
|
||||
[1] We need to alter Nix hardening to be able to statically compile: PIE,
|
||||
Position Independent Executables seems to be supported only on amd64. Having
|
||||
this flags set either make our executables crash or compile as dynamic on many platforms.
|
||||
In the following section codegenOpts, we reactive it for the supported targets
|
||||
(only amd64 curently) through the `-static-pie` flag. PIE is a feature used
|
||||
by ASLR, which helps mitigate security issues.
|
||||
Learn more about Nix Hardening: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh
|
||||
|
||||
[2] We want to inject the git version while keeping the build deterministic.
|
||||
As we do not want to consider the .git folder as part of the input source,
|
||||
we ask the user (the CI often) to pass the value to Nix.
|
||||
*/
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage";
|
||||
overrideAttrs = drv: if git_version != null then {
|
||||
preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
export GIT_VERSION="${git_version}"
|
||||
'';
|
||||
} else {};
|
||||
overrideAttrs = drv:
|
||||
/* [1] */ { hardeningDisable = [ "pie" ]; }
|
||||
//
|
||||
/* [2] */ (if git_version != null then {
|
||||
preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
export GIT_VERSION="${git_version}"
|
||||
'';
|
||||
} else {});
|
||||
})
|
||||
|
||||
/*
|
||||
On a sandbox pure NixOS environment, /usr/bin/file is not available.
|
||||
This is a known problem: https://github.com/NixOS/nixpkgs/issues/98440
|
||||
We simply patch the file as suggested
|
||||
*/
|
||||
/*(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "libsodium-sys";
|
||||
overrideAttrs = drv: {
|
||||
preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
sed -i 's,/usr/bin/file,${file}/bin/file,g' ./configure
|
||||
'';
|
||||
}
|
||||
})*/
|
||||
];
|
||||
|
||||
packageFun = import ./Cargo.nix;
|
||||
|
||||
/*
|
||||
We compile fully static binaries with musl to simplify deployment on most systems.
|
||||
When possible, we reactivate PIE hardening (see above).
|
||||
|
||||
Also, if you set the RUSTFLAGS environment variable, the following parameters will
|
||||
be ignored.
|
||||
|
||||
For more information on static builds, please refer to Rust's RFC 1721.
|
||||
https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
|
||||
*/
|
||||
|
||||
codegenOpts = {
|
||||
"armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */
|
||||
"aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ];
|
||||
};
|
||||
|
||||
/*
|
||||
The following definition is not elegant as we use a low level function of Cargo2nix
|
||||
that enables us to pass our custom rustChannel object. We need this low level definition
|
||||
to pass Nix's Rust toolchains instead of Mozilla's one.
|
||||
|
||||
target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value
|
||||
for each crate.
|
||||
*/
|
||||
rustPkgs = pkgs.rustBuilder.makePackageSet {
|
||||
inherit packageFun rustChannel release;
|
||||
inherit packageFun rustChannel release codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
|
||||
buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet {
|
||||
inherit rustChannel packageFun;
|
||||
inherit rustChannel packageFun codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
};
|
||||
|
||||
localPatterns = [
|
||||
/*
|
||||
The way the default rules are written make think we match recursively, on full path, but the rules are misleading.
|
||||
In fact, the regex is only called on root elements of the crate (and not recursively).
|
||||
This behavior does not work well with our nested modules.
|
||||
We tried to build a "deny list" but negative lookup ahead are not supported on Nix.
|
||||
As a workaround, we have to register all our submodules in this allow list...
|
||||
*/
|
||||
''^(src|tests)'' # fixed default
|
||||
''.*\.(rs|toml)$'' # fixed default
|
||||
''^(crdt|replication|cli|helper|signature)'' # our crate submodules
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
in
|
||||
if compileMode == "test"
|
||||
then builtins.mapAttrs (name: value: rustPkgs.workspace.${name} { inherit compileMode; }) rustPkgs.workspace
|
||||
then pkgs.symlinkJoin {
|
||||
name ="garage-tests";
|
||||
paths = builtins.map (key: rustPkgs.workspace.${key} { inherit compileMode; }) (builtins.attrNames rustPkgs.workspace);
|
||||
}
|
||||
else rustPkgs.workspace.garage { inherit compileMode; }
|
||||
|
|
|
@ -4,9 +4,7 @@ weight = 10
|
|||
+++
|
||||
|
||||
|
||||
Garage is a standard Rust project.
|
||||
First, you need `rust` and `cargo`.
|
||||
For instance on Debian:
|
||||
Garage is a standard Rust project. First, you need `rust` and `cargo`. For instance on Debian:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
|
@ -15,6 +13,13 @@ sudo apt-get install -y rustc cargo
|
|||
|
||||
You can also use [Rustup](https://rustup.rs/) to setup a Rust toolchain easily.
|
||||
|
||||
In addition, you will need a full C toolchain. On Debian-based distributions, it can be installed as follows:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
|
||||
## Using source from `crates.io`
|
||||
|
||||
Garage's source code is published on `crates.io`, Rust's official package repository.
|
||||
|
|
|
@ -32,9 +32,10 @@ We generate the following binary artifacts for now:
|
|||
- **os**: linux
|
||||
- **format**: static binary, docker container
|
||||
|
||||
Additionnaly we also build two web pages:
|
||||
Additionnaly we also build two web pages and one JSON document:
|
||||
- the documentation (this website)
|
||||
- [the release page](https://garagehq.deuxfleurs.fr/_releases.html)
|
||||
- [the release list in JSON format](https://garagehq.deuxfleurs.fr/_releases.json)
|
||||
|
||||
We publish the static binaries on our own garage cluster (you can access them through the releases page)
|
||||
and the docker containers on Docker Hub.
|
||||
|
|
|
@ -20,7 +20,7 @@ as it provides no redundancy for your data!
|
|||
|
||||
Download the latest Garage binary from the release pages on our repository:
|
||||
|
||||
<https://garagehq.deuxfleurs.fr/_releases.html>
|
||||
<https://garagehq.deuxfleurs.fr/download/>
|
||||
|
||||
Place this binary somewhere in your `$PATH` so that you can invoke the `garage`
|
||||
command directly (for instance you can copy the binary in `/usr/local/bin`
|
||||
|
|
|
@ -40,7 +40,6 @@ root_domain = ".s3.garage"
|
|||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
The following gives details about each available configuration option.
|
||||
|
@ -60,19 +59,22 @@ Store this folder on a fast SSD drive if possible to maximize Garage's performan
|
|||
The directory in which Garage will store the data blocks of objects.
|
||||
This folder can be placed on an HDD. The space available for `data_dir`
|
||||
should be counted to determine a node's capacity
|
||||
when [configuring it](@/documentation/cookbook/real-world.md).
|
||||
when [adding it to the cluster layout](@/documentation/cookbook/real-world.md).
|
||||
|
||||
### `block_size`
|
||||
|
||||
Garage splits stored objects in consecutive chunks of size `block_size`
|
||||
(except the last one which might be smaller). The default size is 1MB and
|
||||
should work in most cases. If you are interested in tuning this, feel free
|
||||
to do so (and remember to report your findings to us!). If this value is
|
||||
changed for a running Garage installation, only files newly uploaded will be
|
||||
affected. Previously uploaded files will remain available. This however
|
||||
means that chunks from existing files will not be deduplicated with chunks
|
||||
from newly uploaded files, meaning you might use more storage space that is
|
||||
optimally possible.
|
||||
should work in most cases. We recommend increasing it to e.g. 10MB if
|
||||
you are using Garage to store large files and have fast network connections
|
||||
between all nodes (e.g. 1gbps).
|
||||
|
||||
If you are interested in tuning this, feel free to do so (and remember to
|
||||
report your findings to us!). When this value is changed for a running Garage
|
||||
installation, only files newly uploaded will be affected. Previously uploaded
|
||||
files will remain available. This however means that chunks from existing files
|
||||
will not be deduplicated with chunks from newly uploaded files, meaning you
|
||||
might use more storage space that is optimally possible.
|
||||
|
||||
### `replication_mode`
|
||||
|
||||
|
@ -114,12 +116,12 @@ default value (currently `3`). Finally, zstd has also compression designed to be
|
|||
than default compression levels, they range from `-1` (smaller file) to `-99` (faster
|
||||
compression).
|
||||
|
||||
If you do not specify a `compression_level` entry, garage will set it to `1` for you. With
|
||||
If you do not specify a `compression_level` entry, Garage will set it to `1` for you. With
|
||||
this parameters, zstd consumes low amount of cpu and should work faster than line speed in
|
||||
most situations, while saving some space and intra-cluster
|
||||
bandwidth.
|
||||
|
||||
If you want to totally deactivate zstd in garage, you can pass the special value `'none'`. No
|
||||
If you want to totally deactivate zstd in Garage, you can pass the special value `'none'`. No
|
||||
zstd related code will be called, your chunks will be stored on disk without any processing.
|
||||
|
||||
Compression is done synchronously, setting a value too high will add latency to write queries.
|
||||
|
@ -169,21 +171,23 @@ yourself.
|
|||
|
||||
### `consul_host` and `consul_service_name`
|
||||
|
||||
Garage supports discovering other nodes of the cluster using Consul.
|
||||
This works only when nodes are announced in Consul by an orchestrator such as Nomad,
|
||||
as Garage is not able to announce itself.
|
||||
Garage supports discovering other nodes of the cluster using Consul. For this
|
||||
to work correctly, nodes need to know their IP address by which they can be
|
||||
reached by other nodes of the cluster, which should be set in `rpc_public_addr`.
|
||||
|
||||
The `consul_host` parameter should be set to the hostname of the Consul server,
|
||||
and `consul_service_name` should be set to the service name under which Garage's
|
||||
RPC ports are announced.
|
||||
|
||||
Garage does not yet support talking to Consul over TLS.
|
||||
|
||||
### `sled_cache_capacity`
|
||||
|
||||
This parameter can be used to tune the capacity of the cache used by
|
||||
[sled](https://sled.rs), the database Garage uses internally to store metadata.
|
||||
Tune this to fit the RAM you wish to make available to your Garage instance.
|
||||
More cache means faster Garage, but the default value (128MB) should be plenty
|
||||
for most use cases.
|
||||
This value has a conservative default (128MB) so that Garage doesn't use too much
|
||||
RAM by default, but feel free to increase this for higher performance.
|
||||
|
||||
### `sled_flush_every_ms`
|
||||
|
||||
|
|
BIN
doc/logo/garage_hires.png
Normal file
After Width: | Height: | Size: 30 KiB |
13
doc/talks/2022-02-06-fosdem/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
*
|
||||
|
||||
!assets
|
||||
|
||||
!.gitignore
|
||||
!*.svg
|
||||
!*.png
|
||||
!*.jpg
|
||||
!*.tex
|
||||
!Makefile
|
||||
!.gitignore
|
||||
|
||||
!talk.pdf
|
3
doc/talks/2022-02-06-fosdem/Makefile
Normal file
|
@ -0,0 +1,3 @@
|
|||
talk.pdf: talk.tex
|
||||
pdflatex talk.tex
|
||||
|
BIN
doc/talks/2022-02-06-fosdem/assets/AGPLv3_Logo.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/atuin.jpg
Normal file
After Width: | Height: | Size: 263 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/compatibility.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/endpoint-latency-dc.png
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/garageuses.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/inframap.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/location-aware.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/map.png
Normal file
After Width: | Height: | Size: 145 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/minio.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/neptune.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/rust_logo.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/slide1.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/slide2.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
doc/talks/2022-02-06-fosdem/assets/slide3.png
Normal file
After Width: | Height: | Size: 124 KiB |
4326
doc/talks/2022-02-06-fosdem/assets/slides.svg
Normal file
After Width: | Height: | Size: 315 KiB |
BIN
doc/talks/2022-02-06-fosdem/talk.pdf
(Stored with Git LFS)
Normal file
270
doc/talks/2022-02-06-fosdem/talk.tex
Normal file
|
@ -0,0 +1,270 @@
|
|||
%\nonstopmode
|
||||
\documentclass[aspectratio=169]{beamer}
|
||||
\usepackage[utf8]{inputenc}
|
||||
% \usepackage[frenchb]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{breqn}
|
||||
\usepackage{multirow}
|
||||
\usetheme{boxes}
|
||||
\usepackage{graphicx}
|
||||
%\useoutertheme[footline=authortitle,subsection=false]{miniframes}
|
||||
|
||||
\beamertemplatenavigationsymbolsempty
|
||||
|
||||
\definecolor{TitleOrange}{RGB}{255,137,0}
|
||||
\setbeamercolor{title}{fg=TitleOrange}
|
||||
\setbeamercolor{frametitle}{fg=TitleOrange}
|
||||
|
||||
\definecolor{ListOrange}{RGB}{255,145,5}
|
||||
\setbeamertemplate{itemize item}{\color{ListOrange}$\blacktriangleright$}
|
||||
|
||||
\definecolor{verygrey}{RGB}{70,70,70}
|
||||
\setbeamercolor{normal text}{fg=verygrey}
|
||||
|
||||
|
||||
\usepackage{tabu}
|
||||
\usepackage{multicol}
|
||||
\usepackage{vwcol}
|
||||
\usepackage{stmaryrd}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\usepackage[normalem]{ulem}
|
||||
|
||||
\title{Introducing Garage}
|
||||
\subtitle{a new storage platform for self-hosted geo-distributed clusters}
|
||||
\author{Deuxfleurs Association}
|
||||
\date{FOSDEM '22}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{frame}
|
||||
\centering
|
||||
\includegraphics[width=.3\linewidth]{../../sticker/Garage.pdf}
|
||||
\vspace{1em}
|
||||
|
||||
{\large\bf Deuxfleurs Association}
|
||||
\vspace{1em}
|
||||
|
||||
\url{https://garagehq.deuxfleurs.fr/}
|
||||
|
||||
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Our objective at Deuxfleurs}
|
||||
|
||||
\begin{center}
|
||||
\textbf{Promote self-hosting and small-scale hosting\\
|
||||
as an alternative to large cloud providers}
|
||||
\end{center}
|
||||
\vspace{2em}
|
||||
\visible<2->{
|
||||
Why is it hard?
|
||||
}
|
||||
\visible<3->{
|
||||
\vspace{2em}
|
||||
\begin{center}
|
||||
\textbf{\underline{Resilience}}\\
|
||||
{\footnotesize (we want good uptime/availability with low supervision)}
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How to be resilient (the hard way)}
|
||||
|
||||
Entreprise-grade systems typically employ:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item RAID
|
||||
\item Redundant power grid + UPS
|
||||
\item Redundant Internet connections
|
||||
\item Low-latency links
|
||||
\item ...
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
$\to$ it's costly and only worth it at DC scale
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How to be resilient (the \underline{\textbf{cheap}} way)}
|
||||
|
||||
\only<1,4-5>{
|
||||
Instead, we use:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item \textcolor<2->{gray}{Commodity hardware (e.g. old desktop PCs)}
|
||||
\vspace{.5em}
|
||||
\item<4-> \textcolor<5->{gray}{Commodity Internet (e.g. FTTB, FTTH) and power grid}
|
||||
\vspace{.5em}
|
||||
\item<5-> \textcolor<6->{gray}{\textbf{Geographical redundancy} (multi-site replication)}
|
||||
\end{itemize}
|
||||
}
|
||||
\only<2>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{assets/atuin.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<3>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{assets/neptune.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<6>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.5\linewidth]{assets/inframap.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How to make this happen}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.8\linewidth]{assets/slide1.png}}%
|
||||
\only<2>{\includegraphics[width=.8\linewidth]{assets/slide2.png}}%
|
||||
\only<3>{\includegraphics[width=.8\linewidth]{assets/slide3.png}}%
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Distributed file systems are slow}
|
||||
File systems are complex, for example:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Concurrent modification by several processes
|
||||
\vspace{1em}
|
||||
\item Folder hierarchies
|
||||
\vspace{1em}
|
||||
\item Other requirements of the POSIX spec
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
Coordination in a distributed system is costly
|
||||
|
||||
\vspace{1em}
|
||||
Costs explode with commodity hardware / Internet connections\\
|
||||
{\small (we experienced this!)}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{A simpler solution: object storage}
|
||||
Only two operations:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Put an object at a key
|
||||
\vspace{1em}
|
||||
\item Retrieve an object from its key
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
{\footnotesize (and a few others)}
|
||||
|
||||
\vspace{1em}
|
||||
Sufficient for many applications!
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{A simpler solution: object storage}
|
||||
\begin{center}
|
||||
\includegraphics[width=.2\linewidth]{../2020-12-02_wide-team/img/Amazon-S3.jpg}
|
||||
\hspace{5em}
|
||||
\includegraphics[width=.2\linewidth]{assets/minio.png}
|
||||
\end{center}
|
||||
\vspace{1em}
|
||||
S3: a de-facto standard, many compatible applications
|
||||
|
||||
\vspace{1em}
|
||||
|
||||
MinIO is self-hostable but not suited for geo-distributed deployments
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{But what is Garage, exactly?}
|
||||
\textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}\\
|
||||
\vspace{.5em}
|
||||
that implements resilience through geographical redundancy on commodity hardware
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{assets/garageuses.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What makes Garage different?}
|
||||
\textbf{Coordination-free:}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item No Raft or Paxos
|
||||
\vspace{1em}
|
||||
\item Internal data types are CRDTs
|
||||
\vspace{1em}
|
||||
\item All nodes are equivalent (no master/leader/index node)
|
||||
\end{itemize}
|
||||
\vspace{2em}
|
||||
$\to$ less sensitive to higher latencies between nodes
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What makes Garage different?}
|
||||
\begin{center}
|
||||
\includegraphics[width=.9\linewidth]{assets/endpoint-latency-dc.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What makes Garage different?}
|
||||
\textbf{Consistency model:}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item Not ACID (not required by S3 spec) / not linearizable
|
||||
\vspace{1em}
|
||||
\item \textbf{Read-after-write consistency}\\
|
||||
{\footnotesize (stronger than eventual consistency)}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What makes Garage different?}
|
||||
\textbf{Location-aware:}
|
||||
\vspace{2em}
|
||||
\begin{center}
|
||||
\includegraphics[width=\linewidth]{assets/location-aware.png}
|
||||
\end{center}
|
||||
\vspace{2em}
|
||||
Garage replicates data on different zones when possible
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What makes Garage different?}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{assets/map.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{An ever-increasing compatibility list}
|
||||
\begin{center}
|
||||
\includegraphics[width=.7\linewidth]{assets/compatibility.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Get Garage now!}
|
||||
\begin{center}
|
||||
\includegraphics[width=.3\linewidth]{../../logo/garage_hires.png}\\
|
||||
\vspace{-1em}
|
||||
\url{https://garagehq.deuxfleurs.fr/}\\
|
||||
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
|
||||
|
||||
\vspace{2em}
|
||||
\includegraphics[width=.09\linewidth]{assets/rust_logo.png}
|
||||
\includegraphics[width=.2\linewidth]{assets/AGPLv3_Logo.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Demo time!}
|
||||
\end{frame}
|
||||
|
||||
\end{document}
|
||||
|
||||
%% vim: set ts=4 sw=4 tw=0 noet spelllang=fr :
|
|
@ -8,11 +8,14 @@ rec {
|
|||
sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr";
|
||||
};
|
||||
cargo2nixSrc = fetchGit {
|
||||
# As of 2021-10-06
|
||||
# As of 2022-02-03
|
||||
url = "https://github.com/superboum/cargo2nix";
|
||||
rev = "1364752cd784764db2ef5b1e1248727cebfae2ce";
|
||||
ref = "backward-compat";
|
||||
rev = "08d963f32a774353ee8acf3f61749915875c1ec4";
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Shared objects
|
||||
*/
|
||||
|
|
|
@ -76,7 +76,7 @@ function refresh_toolchain {
|
|||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
/*(pkgs.callPackage cargo2nix {}).package*/
|
||||
cargo2nix.packages.x86_64-linux.cargo2nix
|
||||
] else [])
|
||||
++
|
||||
(if integration then [
|
||||
|
|
|
@ -36,13 +36,16 @@ futures-util = "0.3"
|
|||
pin-project = "1.0"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
form_urlencoded = "1.0.0"
|
||||
http = "0.2"
|
||||
httpdate = "0.3"
|
||||
http-range = "0.1"
|
||||
hyper = { version = "0.14", features = ["server", "http1", "runtime", "tcp", "stream"] }
|
||||
multer = "2.0"
|
||||
percent-encoding = "2.1.0"
|
||||
roxmltree = "0.14"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1.0"
|
||||
quick-xml = { version = "0.21", features = [ "serialize" ] }
|
||||
url = "2.1"
|
||||
|
|
|
@ -25,6 +25,7 @@ use crate::s3_cors::*;
|
|||
use crate::s3_delete::*;
|
||||
use crate::s3_get::*;
|
||||
use crate::s3_list::*;
|
||||
use crate::s3_post_object::handle_post_object;
|
||||
use crate::s3_put::*;
|
||||
use crate::s3_router::{Authorization, Endpoint};
|
||||
use crate::s3_website::*;
|
||||
|
@ -92,11 +93,6 @@ async fn handler(
|
|||
}
|
||||
|
||||
async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let (api_key, content_sha256) = check_payload_signature(&garage, &req).await?;
|
||||
let api_key = api_key.ok_or_else(|| {
|
||||
Error::Forbidden("Garage does not support anonymous access yet".to_string())
|
||||
})?;
|
||||
|
||||
let authority = req
|
||||
.headers()
|
||||
.get(header::HOST)
|
||||
|
@ -115,6 +111,19 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
|||
let (endpoint, bucket_name) = Endpoint::from_request(&req, bucket_name.map(ToOwned::to_owned))?;
|
||||
debug!("Endpoint: {:?}", endpoint);
|
||||
|
||||
// Some endpoints are processed early, before we even check for an API key
|
||||
if let Endpoint::PostObject = endpoint {
|
||||
return handle_post_object(garage, req, bucket_name.unwrap()).await;
|
||||
}
|
||||
if let Endpoint::Options = endpoint {
|
||||
return handle_options_s3api(garage, &req, bucket_name).await;
|
||||
}
|
||||
|
||||
let (api_key, content_sha256) = check_payload_signature(&garage, &req).await?;
|
||||
let api_key = api_key.ok_or_else(|| {
|
||||
Error::Forbidden("Garage does not support anonymous access yet".to_string())
|
||||
})?;
|
||||
|
||||
let bucket_name = match bucket_name {
|
||||
None => return handle_request_without_bucket(garage, req, api_key, endpoint).await,
|
||||
Some(bucket) => bucket.to_string(),
|
||||
|
@ -156,7 +165,6 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
|||
};
|
||||
|
||||
let resp = match endpoint {
|
||||
Endpoint::Options => handle_options(&req, &bucket).await,
|
||||
Endpoint::HeadObject {
|
||||
key, part_number, ..
|
||||
} => handle_head(garage, &req, bucket_id, &key, part_number).await,
|
||||
|
|
|
@ -126,6 +126,12 @@ impl From<HelperError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<multer::Error> for Error {
|
||||
fn from(err: multer::Error) -> Self {
|
||||
Self::BadRequest(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Get the HTTP status code that best represents the meaning of the error for the client
|
||||
pub fn http_status_code(&self) -> StatusCode {
|
||||
|
|
|
@ -19,6 +19,7 @@ pub mod s3_cors;
|
|||
mod s3_delete;
|
||||
pub mod s3_get;
|
||||
mod s3_list;
|
||||
mod s3_post_object;
|
||||
mod s3_put;
|
||||
mod s3_router;
|
||||
mod s3_website;
|
||||
|
|
|
@ -46,7 +46,7 @@ pub async fn handle_copy(
|
|||
// Implement x-amz-metadata-directive: REPLACE
|
||||
let new_meta = match req.headers().get("x-amz-metadata-directive") {
|
||||
Some(v) if v == hyper::header::HeaderValue::from_static("REPLACE") => ObjectVersionMeta {
|
||||
headers: get_headers(req)?,
|
||||
headers: get_headers(req.headers())?,
|
||||
size: source_version_meta.size,
|
||||
etag: source_version_meta.etag.clone(),
|
||||
},
|
||||
|
|
|
@ -100,7 +100,63 @@ pub async fn handle_put_cors(
|
|||
.body(Body::empty())?)
|
||||
}
|
||||
|
||||
pub async fn handle_options(req: &Request<Body>, bucket: &Bucket) -> Result<Response<Body>, Error> {
|
||||
pub async fn handle_options_s3api(
|
||||
garage: Arc<Garage>,
|
||||
req: &Request<Body>,
|
||||
bucket_name: Option<String>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
// FIXME: CORS rules of buckets with local aliases are
|
||||
// not taken into account.
|
||||
|
||||
// If the bucket name is a global bucket name,
|
||||
// we try to apply the CORS rules of that bucket.
|
||||
// If a user has a local bucket name that has
|
||||
// the same name, its CORS rules won't be applied
|
||||
// and will be shadowed by the rules of the globally
|
||||
// existing bucket (but this is inevitable because
|
||||
// OPTIONS calls are not auhtenticated).
|
||||
if let Some(bn) = bucket_name {
|
||||
let helper = garage.bucket_helper();
|
||||
let bucket_id = helper.resolve_global_bucket_name(&bn).await?;
|
||||
if let Some(id) = bucket_id {
|
||||
let bucket = garage
|
||||
.bucket_table
|
||||
.get(&EmptyKey, &id)
|
||||
.await?
|
||||
.filter(|b| !b.state.is_deleted())
|
||||
.ok_or(Error::NoSuchBucket)?;
|
||||
handle_options_for_bucket(req, &bucket)
|
||||
} else {
|
||||
// If there is a bucket name in the request, but that name
|
||||
// does not correspond to a global alias for a bucket,
|
||||
// then it's either a non-existing bucket or a local bucket.
|
||||
// We have no way of knowing, because the request is not
|
||||
// authenticated and thus we can't resolve local aliases.
|
||||
// We take the permissive approach of allowing everything,
|
||||
// because we don't want to prevent web apps that use
|
||||
// local bucket names from making API calls.
|
||||
Ok(Response::builder()
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "*")
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())?)
|
||||
}
|
||||
} else {
|
||||
// If there is no bucket name in the request,
|
||||
// we are doing a ListBuckets call, which we want to allow
|
||||
// for all origins.
|
||||
Ok(Response::builder()
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "GET")
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_options_for_bucket(
|
||||
req: &Request<Body>,
|
||||
bucket: &Bucket,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let origin = req
|
||||
.headers()
|
||||
.get("Origin")
|
||||
|
|
499
src/api/s3_post_object.rs
Normal file
|
@ -0,0 +1,499 @@
|
|||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use futures::{Stream, StreamExt};
|
||||
use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use multer::{Constraints, Multipart, SizeLimit};
|
||||
use serde::Deserialize;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use crate::api_server::resolve_bucket;
|
||||
use crate::error::*;
|
||||
use crate::s3_put::{get_headers, save_stream};
|
||||
use crate::s3_xml;
|
||||
use crate::signature::payload::{parse_date, verify_v4};
|
||||
|
||||
pub async fn handle_post_object(
|
||||
garage: Arc<Garage>,
|
||||
req: Request<Body>,
|
||||
bucket: String,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let boundary = req
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
.and_then(|ct| multer::parse_boundary(ct).ok())
|
||||
.ok_or_bad_request("Counld not get multipart boundary")?;
|
||||
|
||||
// 16k seems plenty for a header. 5G is the max size of a single part, so it seems reasonable
|
||||
// for a PostObject
|
||||
let constraints = Constraints::new().size_limit(
|
||||
SizeLimit::new()
|
||||
.per_field(16 * 1024)
|
||||
.for_field("file", 5 * 1024 * 1024 * 1024),
|
||||
);
|
||||
|
||||
let (head, body) = req.into_parts();
|
||||
let mut multipart = Multipart::with_constraints(body, boundary, constraints);
|
||||
|
||||
let mut params = HeaderMap::new();
|
||||
let field = loop {
|
||||
let field = if let Some(field) = multipart.next_field().await? {
|
||||
field
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
"Request did not contain a file".to_owned(),
|
||||
));
|
||||
};
|
||||
let name: HeaderName = if let Some(Ok(name)) = field.name().map(TryInto::try_into) {
|
||||
name
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if name == "file" {
|
||||
break field;
|
||||
}
|
||||
|
||||
if let Ok(content) = HeaderValue::from_str(&field.text().await?) {
|
||||
match name.as_str() {
|
||||
"tag" => (/* tag need to be reencoded, but we don't support them yet anyway */),
|
||||
"acl" => {
|
||||
if params.insert("x-amz-acl", content).is_some() {
|
||||
return Err(Error::BadRequest(
|
||||
"Field 'acl' provided more than one time".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if params.insert(&name, content).is_some() {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Field '{}' provided more than one time",
|
||||
name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Current part is file. Do some checks before handling to PutObject code
|
||||
let key = params
|
||||
.get("key")
|
||||
.ok_or_bad_request("No key was provided")?
|
||||
.to_str()?;
|
||||
let credential = params
|
||||
.get("x-amz-credential")
|
||||
.ok_or_else(|| {
|
||||
Error::Forbidden("Garage does not support anonymous access yet".to_string())
|
||||
})?
|
||||
.to_str()?;
|
||||
let policy = params
|
||||
.get("policy")
|
||||
.ok_or_bad_request("No policy was provided")?
|
||||
.to_str()?;
|
||||
let signature = params
|
||||
.get("x-amz-signature")
|
||||
.ok_or_bad_request("No signature was provided")?
|
||||
.to_str()?;
|
||||
let date = params
|
||||
.get("x-amz-date")
|
||||
.ok_or_bad_request("No date was provided")?
|
||||
.to_str()?;
|
||||
|
||||
let key = if key.contains("${filename}") {
|
||||
// if no filename is provided, don't replace. This matches the behavior of AWS.
|
||||
if let Some(filename) = field.file_name() {
|
||||
key.replace("${filename}", filename)
|
||||
} else {
|
||||
key.to_owned()
|
||||
}
|
||||
} else {
|
||||
key.to_owned()
|
||||
};
|
||||
|
||||
let date = parse_date(date)?;
|
||||
let api_key = verify_v4(&garage, credential, &date, signature, policy.as_bytes()).await?;
|
||||
|
||||
let bucket_id = resolve_bucket(&garage, &bucket, &api_key).await?;
|
||||
|
||||
if !api_key.allow_write(&bucket_id) {
|
||||
return Err(Error::Forbidden(
|
||||
"Operation is not allowed for this key.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let decoded_policy = base64::decode(&policy)?;
|
||||
let decoded_policy: Policy =
|
||||
serde_json::from_slice(&decoded_policy).ok_or_bad_request("Invalid policy")?;
|
||||
|
||||
let expiration: DateTime<Utc> = DateTime::parse_from_rfc3339(&decoded_policy.expiration)
|
||||
.ok_or_bad_request("Invalid expiration date")?
|
||||
.into();
|
||||
if Utc::now() - expiration > Duration::zero() {
|
||||
return Err(Error::BadRequest(
|
||||
"Expiration date is in the paste".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut conditions = decoded_policy.into_conditions()?;
|
||||
|
||||
for (param_key, value) in params.iter() {
|
||||
let mut param_key = param_key.to_string();
|
||||
param_key.make_ascii_lowercase();
|
||||
match param_key.as_str() {
|
||||
"policy" | "x-amz-signature" => (), // this is always accepted, as it's required to validate other fields
|
||||
"content-type" => {
|
||||
let conds = conditions.params.remove("content-type").ok_or_else(|| {
|
||||
Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key))
|
||||
})?;
|
||||
for cond in conds {
|
||||
let ok = match cond {
|
||||
Operation::Equal(s) => s.as_str() == value,
|
||||
Operation::StartsWith(s) => {
|
||||
value.to_str()?.split(',').all(|v| v.starts_with(&s))
|
||||
}
|
||||
};
|
||||
if !ok {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Key '{}' has value not allowed in policy",
|
||||
param_key
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
"key" => {
|
||||
let conds = conditions.params.remove("key").ok_or_else(|| {
|
||||
Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key))
|
||||
})?;
|
||||
for cond in conds {
|
||||
let ok = match cond {
|
||||
Operation::Equal(s) => s == key,
|
||||
Operation::StartsWith(s) => key.starts_with(&s),
|
||||
};
|
||||
if !ok {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Key '{}' has value not allowed in policy",
|
||||
param_key
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if param_key.starts_with("x-ignore-") {
|
||||
// if a x-ignore is provided in policy, it's not removed here, so it will be
|
||||
// rejected as provided in policy but not in the request. As odd as it is, it's
|
||||
// how aws seems to behave.
|
||||
continue;
|
||||
}
|
||||
let conds = conditions.params.remove(¶m_key).ok_or_else(|| {
|
||||
Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key))
|
||||
})?;
|
||||
for cond in conds {
|
||||
let ok = match cond {
|
||||
Operation::Equal(s) => s.as_str() == value,
|
||||
Operation::StartsWith(s) => value.to_str()?.starts_with(s.as_str()),
|
||||
};
|
||||
if !ok {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Key '{}' has value not allowed in policy",
|
||||
param_key
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((param_key, _)) = conditions.params.iter().next() {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Key '{}' is required in policy, but no value was provided",
|
||||
param_key
|
||||
)));
|
||||
}
|
||||
|
||||
let headers = get_headers(¶ms)?;
|
||||
|
||||
let stream = field.map(|r| r.map_err(Into::into));
|
||||
let (_, md5) = save_stream(
|
||||
garage,
|
||||
headers,
|
||||
StreamLimiter::new(stream, conditions.content_length),
|
||||
bucket_id,
|
||||
&key,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let etag = format!("\"{}\"", md5);
|
||||
|
||||
let resp = if let Some(mut target) = params
|
||||
.get("success_action_redirect")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|u| url::Url::parse(u).ok())
|
||||
.filter(|u| u.scheme() == "https" || u.scheme() == "http")
|
||||
{
|
||||
target
|
||||
.query_pairs_mut()
|
||||
.append_pair("bucket", &bucket)
|
||||
.append_pair("key", &key)
|
||||
.append_pair("etag", &etag);
|
||||
let target = target.to_string();
|
||||
Response::builder()
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header(header::LOCATION, target.clone())
|
||||
.header(header::ETAG, etag)
|
||||
.body(target.into())?
|
||||
} else {
|
||||
let path = head
|
||||
.uri
|
||||
.into_parts()
|
||||
.path_and_query
|
||||
.map(|paq| paq.path().to_string())
|
||||
.unwrap_or_else(|| "/".to_string());
|
||||
let authority = head
|
||||
.headers
|
||||
.get(header::HOST)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
let proto = if !authority.is_empty() {
|
||||
"https://"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let url_key: String = form_urlencoded::byte_serialize(key.as_bytes())
|
||||
.flat_map(str::chars)
|
||||
.collect();
|
||||
let location = format!("{}{}{}{}", proto, authority, path, url_key);
|
||||
|
||||
let action = params
|
||||
.get("success_action_status")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("204");
|
||||
let builder = Response::builder()
|
||||
.header(header::LOCATION, location.clone())
|
||||
.header(header::ETAG, etag.clone());
|
||||
match action {
|
||||
"200" => builder.status(StatusCode::OK).body(Body::empty())?,
|
||||
"201" => {
|
||||
let xml = s3_xml::PostObject {
|
||||
xmlns: (),
|
||||
location: s3_xml::Value(location),
|
||||
bucket: s3_xml::Value(bucket),
|
||||
key: s3_xml::Value(key),
|
||||
etag: s3_xml::Value(etag),
|
||||
};
|
||||
let body = s3_xml::to_xml_with_header(&xml)?;
|
||||
builder
|
||||
.status(StatusCode::CREATED)
|
||||
.body(Body::from(body.into_bytes()))?
|
||||
}
|
||||
_ => builder.status(StatusCode::NO_CONTENT).body(Body::empty())?,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Policy {
|
||||
expiration: String,
|
||||
conditions: Vec<PolicyCondition>,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
fn into_conditions(self) -> Result<Conditions, Error> {
|
||||
let mut params = HashMap::<_, Vec<_>>::new();
|
||||
|
||||
let mut length = (0, u64::MAX);
|
||||
for condition in self.conditions {
|
||||
match condition {
|
||||
PolicyCondition::Equal(map) => {
|
||||
if map.len() != 1 {
|
||||
return Err(Error::BadRequest("Invalid policy item".to_owned()));
|
||||
}
|
||||
let (mut k, v) = map.into_iter().next().expect("size was verified");
|
||||
k.make_ascii_lowercase();
|
||||
params.entry(k).or_default().push(Operation::Equal(v));
|
||||
}
|
||||
PolicyCondition::OtherOp([cond, mut key, value]) => {
|
||||
if key.remove(0) != '$' {
|
||||
return Err(Error::BadRequest("Invalid policy item".to_owned()));
|
||||
}
|
||||
key.make_ascii_lowercase();
|
||||
match cond.as_str() {
|
||||
"eq" => {
|
||||
params.entry(key).or_default().push(Operation::Equal(value));
|
||||
}
|
||||
"starts-with" => {
|
||||
params
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(Operation::StartsWith(value));
|
||||
}
|
||||
_ => return Err(Error::BadRequest("Invalid policy item".to_owned())),
|
||||
}
|
||||
}
|
||||
PolicyCondition::SizeRange(key, min, max) => {
|
||||
if key == "content-length-range" {
|
||||
length.0 = length.0.max(min);
|
||||
length.1 = length.1.min(max);
|
||||
} else {
|
||||
return Err(Error::BadRequest("Invalid policy item".to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Conditions {
|
||||
params,
|
||||
content_length: RangeInclusive::new(length.0, length.1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A single condition from a policy
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PolicyCondition {
|
||||
// will contain a single key-value pair
|
||||
Equal(HashMap<String, String>),
|
||||
OtherOp([String; 3]),
|
||||
SizeRange(String, u64, u64),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Conditions {
|
||||
params: HashMap<String, Vec<Operation>>,
|
||||
content_length: RangeInclusive<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Operation {
|
||||
Equal(String),
|
||||
StartsWith(String),
|
||||
}
|
||||
|
||||
struct StreamLimiter<T> {
|
||||
inner: T,
|
||||
length: RangeInclusive<u64>,
|
||||
read: u64,
|
||||
}
|
||||
|
||||
impl<T> StreamLimiter<T> {
|
||||
fn new(stream: T, length: RangeInclusive<u64>) -> Self {
|
||||
StreamLimiter {
|
||||
inner: stream,
|
||||
length,
|
||||
read: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Stream for StreamLimiter<T>
|
||||
where
|
||||
T: Stream<Item = Result<Bytes, Error>> + Unpin,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let res = std::pin::Pin::new(&mut self.inner).poll_next(ctx);
|
||||
match &res {
|
||||
Poll::Ready(Some(Ok(bytes))) => {
|
||||
self.read += bytes.len() as u64;
|
||||
// optimization to fail early when we know before the end it's too long
|
||||
if self.length.end() < &self.read {
|
||||
return Poll::Ready(Some(Err(Error::BadRequest(
|
||||
"File size does not match policy".to_owned(),
|
||||
))));
|
||||
}
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
if !self.length.contains(&self.read) {
|
||||
return Poll::Ready(Some(Err(Error::BadRequest(
|
||||
"File size does not match policy".to_owned(),
|
||||
))));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_policy_1() {
|
||||
let policy_json = br#"
|
||||
{ "expiration": "2007-12-01T12:00:00.000Z",
|
||||
"conditions": [
|
||||
{"acl": "public-read" },
|
||||
{"bucket": "johnsmith" },
|
||||
["starts-with", "$key", "user/eric/"]
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let policy_2: Policy = serde_json::from_slice(&policy_json[..]).unwrap();
|
||||
let mut conditions = policy_2.into_conditions().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
conditions.params.remove(&"acl".to_string()),
|
||||
Some(vec![Operation::Equal("public-read".into())])
|
||||
);
|
||||
assert_eq!(
|
||||
conditions.params.remove(&"bucket".to_string()),
|
||||
Some(vec![Operation::Equal("johnsmith".into())])
|
||||
);
|
||||
assert_eq!(
|
||||
conditions.params.remove(&"key".to_string()),
|
||||
Some(vec![Operation::StartsWith("user/eric/".into())])
|
||||
);
|
||||
assert!(conditions.params.is_empty());
|
||||
assert_eq!(conditions.content_length, 0..=u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_policy_2() {
|
||||
let policy_json = br#"
|
||||
{ "expiration": "2007-12-01T12:00:00.000Z",
|
||||
"conditions": [
|
||||
[ "eq", "$acl", "public-read" ],
|
||||
["starts-with", "$Content-Type", "image/"],
|
||||
["starts-with", "$success_action_redirect", ""],
|
||||
["content-length-range", 1048576, 10485760]
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let policy_2: Policy = serde_json::from_slice(&policy_json[..]).unwrap();
|
||||
let mut conditions = policy_2.into_conditions().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
conditions.params.remove(&"acl".to_string()),
|
||||
Some(vec![Operation::Equal("public-read".into())])
|
||||
);
|
||||
assert_eq!(
|
||||
conditions.params.remove("content-type").unwrap(),
|
||||
vec![Operation::StartsWith("image/".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
conditions
|
||||
.params
|
||||
.remove(&"success_action_redirect".to_string()),
|
||||
Some(vec![Operation::StartsWith("".into())])
|
||||
);
|
||||
assert!(conditions.params.is_empty());
|
||||
assert_eq!(conditions.content_length, 1048576..=10485760);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use futures::{prelude::*, TryFutureExt};
|
||||
use hyper::body::{Body, Bytes};
|
||||
use hyper::header::{HeaderMap, HeaderValue};
|
||||
use hyper::{Request, Response};
|
||||
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
||||
use sha2::Sha256;
|
||||
|
@ -34,12 +35,8 @@ pub async fn handle_put(
|
|||
api_key: &Key,
|
||||
mut content_sha256: Option<Hash>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
// Generate identity of new version
|
||||
let version_uuid = gen_uuid();
|
||||
let version_timestamp = now_msec();
|
||||
|
||||
// Retrieve interesting headers from request
|
||||
let headers = get_headers(&req)?;
|
||||
let headers = get_headers(req.headers())?;
|
||||
debug!("Object headers: {:?}", headers);
|
||||
|
||||
let content_md5 = match req.headers().get("content-md5") {
|
||||
|
@ -92,6 +89,32 @@ pub async fn handle_put(
|
|||
body.boxed()
|
||||
};
|
||||
|
||||
save_stream(
|
||||
garage,
|
||||
headers,
|
||||
body,
|
||||
bucket_id,
|
||||
key,
|
||||
content_md5,
|
||||
content_sha256,
|
||||
)
|
||||
.await
|
||||
.map(|(uuid, md5)| put_response(uuid, md5))
|
||||
}
|
||||
|
||||
pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
garage: Arc<Garage>,
|
||||
headers: ObjectVersionHeaders,
|
||||
body: S,
|
||||
bucket_id: Uuid,
|
||||
key: &str,
|
||||
content_md5: Option<String>,
|
||||
content_sha256: Option<FixedBytes32>,
|
||||
) -> Result<(Uuid, String), Error> {
|
||||
// Generate identity of new version
|
||||
let version_uuid = gen_uuid();
|
||||
let version_timestamp = now_msec();
|
||||
|
||||
let mut chunker = StreamChunker::new(body, garage.config.block_size);
|
||||
let first_block = chunker.next().await?.unwrap_or_default();
|
||||
|
||||
|
@ -128,7 +151,7 @@ pub async fn handle_put(
|
|||
let object = Object::new(bucket_id, key.into(), vec![object_version]);
|
||||
garage.object_table.insert(&object).await?;
|
||||
|
||||
return Ok(put_response(version_uuid, data_md5sum_hex));
|
||||
return Ok((version_uuid, data_md5sum_hex));
|
||||
}
|
||||
|
||||
// Write version identifier in object table so that we have a trace
|
||||
|
@ -194,7 +217,7 @@ pub async fn handle_put(
|
|||
let object = Object::new(bucket_id, key.into(), vec![object_version]);
|
||||
garage.object_table.insert(&object).await?;
|
||||
|
||||
Ok(put_response(version_uuid, md5sum_hex))
|
||||
Ok((version_uuid, md5sum_hex))
|
||||
}
|
||||
|
||||
/// Validate MD5 sum against content-md5 header
|
||||
|
@ -373,7 +396,7 @@ pub async fn handle_create_multipart_upload(
|
|||
key: &str,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let version_uuid = gen_uuid();
|
||||
let headers = get_headers(req)?;
|
||||
let headers = get_headers(req.headers())?;
|
||||
|
||||
// Create object in object table
|
||||
let object_version = ObjectVersion {
|
||||
|
@ -490,7 +513,7 @@ pub async fn handle_put_part(
|
|||
|
||||
let response = Response::builder()
|
||||
.header("ETag", format!("\"{}\"", data_md5sum_hex))
|
||||
.body(Body::from(vec![]))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
|
@ -672,17 +695,16 @@ pub async fn handle_abort_multipart_upload(
|
|||
Ok(Response::new(Body::from(vec![])))
|
||||
}
|
||||
|
||||
fn get_mime_type(req: &Request<Body>) -> Result<String, Error> {
|
||||
Ok(req
|
||||
.headers()
|
||||
fn get_mime_type(headers: &HeaderMap<HeaderValue>) -> Result<String, Error> {
|
||||
Ok(headers
|
||||
.get(hyper::header::CONTENT_TYPE)
|
||||
.map(|x| x.to_str())
|
||||
.unwrap_or(Ok("blob"))?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get_headers(req: &Request<Body>) -> Result<ObjectVersionHeaders, Error> {
|
||||
let content_type = get_mime_type(req)?;
|
||||
pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<ObjectVersionHeaders, Error> {
|
||||
let content_type = get_mime_type(headers)?;
|
||||
let mut other = BTreeMap::new();
|
||||
|
||||
// Preserve standard headers
|
||||
|
@ -694,7 +716,7 @@ pub(crate) fn get_headers(req: &Request<Body>) -> Result<ObjectVersionHeaders, E
|
|||
hyper::header::EXPIRES,
|
||||
];
|
||||
for h in standard_header.iter() {
|
||||
if let Some(v) = req.headers().get(h) {
|
||||
if let Some(v) = headers.get(h) {
|
||||
match v.to_str() {
|
||||
Ok(v_str) => {
|
||||
other.insert(h.to_string(), v_str.to_string());
|
||||
|
@ -707,7 +729,7 @@ pub(crate) fn get_headers(req: &Request<Body>) -> Result<ObjectVersionHeaders, E
|
|||
}
|
||||
|
||||
// Preserve x-amz-meta- headers
|
||||
for (k, v) in req.headers().iter() {
|
||||
for (k, v) in headers.iter() {
|
||||
if k.as_str().starts_with("x-amz-meta-") {
|
||||
match v.to_str() {
|
||||
Ok(v_str) => {
|
||||
|
|
|
@ -410,6 +410,11 @@ pub enum Endpoint {
|
|||
part_number: u64,
|
||||
upload_id: String,
|
||||
},
|
||||
// This endpoint is not documented with others because it has special use case :
|
||||
// It's intended to be used with HTML forms, using a multipart/form-data body.
|
||||
// It works a lot like presigned requests, but everything is in the form instead
|
||||
// of being query parameters of the URL, so authenticating it is a bit different.
|
||||
PostObject,
|
||||
}}
|
||||
|
||||
impl Endpoint {
|
||||
|
@ -424,7 +429,11 @@ impl Endpoint {
|
|||
let path = uri.path().trim_start_matches('/');
|
||||
let query = uri.query();
|
||||
if bucket.is_none() && path.is_empty() {
|
||||
return Ok((Self::ListBuckets, None));
|
||||
if *req.method() == Method::OPTIONS {
|
||||
return Ok((Self::Options, None));
|
||||
} else {
|
||||
return Ok((Self::ListBuckets, None));
|
||||
}
|
||||
}
|
||||
|
||||
let (bucket, key) = if let Some(bucket) = bucket {
|
||||
|
@ -543,6 +552,7 @@ impl Endpoint {
|
|||
UPLOADS => CreateMultipartUpload,
|
||||
],
|
||||
no_key: [
|
||||
EMPTY => PostObject,
|
||||
DELETE => DeleteObjects,
|
||||
]
|
||||
}
|
||||
|
@ -1165,6 +1175,7 @@ mod tests {
|
|||
POST "/{Key+}?restore&versionId=VersionId" => RestoreObject
|
||||
PUT "/my-movie.m2ts?partNumber=1&uploadId=VCVsb2FkIElEIGZvciBlbZZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZR" => UploadPart
|
||||
PUT "/Key+?partNumber=2&uploadId=UploadId" => UploadPart
|
||||
POST "/" => PostObject
|
||||
);
|
||||
// no bucket, won't work with the rest of the test suite
|
||||
assert!(matches!(
|
||||
|
|
|
@ -289,6 +289,20 @@ pub struct VersioningConfiguration {
|
|||
pub status: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
pub struct PostObject {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
#[serde(rename = "Location")]
|
||||
pub location: Value,
|
||||
#[serde(rename = "Bucket")]
|
||||
pub bucket: Value,
|
||||
#[serde(rename = "Key")]
|
||||
pub key: Value,
|
||||
#[serde(rename = "ETag")]
|
||||
pub etag: Value,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -49,23 +49,6 @@ pub async fn check_payload_signature(
|
|||
}
|
||||
};
|
||||
|
||||
let scope = format!(
|
||||
"{}/{}/s3/aws4_request",
|
||||
authorization.date.format(SHORT_DATE),
|
||||
garage.config.s3_api.s3_region
|
||||
);
|
||||
if authorization.scope != scope {
|
||||
return Err(Error::AuthorizationHeaderMalformed(scope.to_string()));
|
||||
}
|
||||
|
||||
let key = garage
|
||||
.key_table
|
||||
.get(&EmptyKey, &authorization.key_id)
|
||||
.await?
|
||||
.filter(|k| !k.state.is_deleted())
|
||||
.ok_or_else(|| Error::Forbidden(format!("No such key: {}", authorization.key_id)))?;
|
||||
let key_p = key.params().unwrap();
|
||||
|
||||
let canonical_request = canonical_request(
|
||||
request.method(),
|
||||
&request.uri().path().to_string(),
|
||||
|
@ -74,24 +57,20 @@ pub async fn check_payload_signature(
|
|||
&authorization.signed_headers,
|
||||
&authorization.content_sha256,
|
||||
);
|
||||
let (_, scope) = parse_credential(&authorization.credential)?;
|
||||
let string_to_sign = string_to_sign(&authorization.date, &scope, &canonical_request);
|
||||
|
||||
let mut hmac = signing_hmac(
|
||||
&authorization.date,
|
||||
&key_p.secret_key,
|
||||
&garage.config.s3_api.s3_region,
|
||||
"s3",
|
||||
)
|
||||
.ok_or_internal_error("Unable to build signing HMAC")?;
|
||||
hmac.update(string_to_sign.as_bytes());
|
||||
let signature = hex::encode(hmac.finalize().into_bytes());
|
||||
trace!("canonical request:\n{}", canonical_request);
|
||||
trace!("string to sign:\n{}", string_to_sign);
|
||||
|
||||
if authorization.signature != signature {
|
||||
trace!("Canonical request: ``{}``", canonical_request);
|
||||
trace!("String to sign: ``{}``", string_to_sign);
|
||||
trace!("Expected: {}, got: {}", signature, authorization.signature);
|
||||
return Err(Error::Forbidden("Invalid signature".to_string()));
|
||||
}
|
||||
let key = verify_v4(
|
||||
garage,
|
||||
&authorization.credential,
|
||||
&authorization.date,
|
||||
&authorization.signature,
|
||||
string_to_sign.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" {
|
||||
None
|
||||
|
@ -108,8 +87,7 @@ pub async fn check_payload_signature(
|
|||
}
|
||||
|
||||
struct Authorization {
|
||||
key_id: String,
|
||||
scope: String,
|
||||
credential: String,
|
||||
signed_headers: String,
|
||||
signature: String,
|
||||
content_sha256: String,
|
||||
|
@ -142,7 +120,6 @@ fn parse_authorization(
|
|||
let cred = auth_params
|
||||
.get("Credential")
|
||||
.ok_or_bad_request("Could not find Credential in Authorization field")?;
|
||||
let (key_id, scope) = parse_credential(cred)?;
|
||||
|
||||
let content_sha256 = headers
|
||||
.get("x-amz-content-sha256")
|
||||
|
@ -150,18 +127,15 @@ fn parse_authorization(
|
|||
|
||||
let date = headers
|
||||
.get("x-amz-date")
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")?;
|
||||
let date: NaiveDateTime =
|
||||
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
||||
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")
|
||||
.and_then(|d| parse_date(d))?;
|
||||
|
||||
if Utc::now() - date > Duration::hours(24) {
|
||||
return Err(Error::BadRequest("Date is too old".to_string()));
|
||||
}
|
||||
|
||||
let auth = Authorization {
|
||||
key_id,
|
||||
scope,
|
||||
credential: cred.to_string(),
|
||||
signed_headers: auth_params
|
||||
.get("SignedHeaders")
|
||||
.ok_or_bad_request("Could not find SignedHeaders in Authorization field")?
|
||||
|
@ -189,7 +163,6 @@ fn parse_query_authorization(
|
|||
let cred = headers
|
||||
.get("x-amz-credential")
|
||||
.ok_or_bad_request("X-Amz-Credential not found in query parameters")?;
|
||||
let (key_id, scope) = parse_credential(cred)?;
|
||||
let signed_headers = headers
|
||||
.get("x-amz-signedheaders")
|
||||
.ok_or_bad_request("X-Amz-SignedHeaders not found in query parameters")?;
|
||||
|
@ -215,18 +188,15 @@ fn parse_query_authorization(
|
|||
|
||||
let date = headers
|
||||
.get("x-amz-date")
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")?;
|
||||
let date: NaiveDateTime =
|
||||
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
||||
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")
|
||||
.and_then(|d| parse_date(d))?;
|
||||
|
||||
if Utc::now() - date > Duration::seconds(duration) {
|
||||
return Err(Error::BadRequest("Date is too old".to_string()));
|
||||
}
|
||||
|
||||
Ok(Authorization {
|
||||
key_id,
|
||||
scope,
|
||||
credential: cred.to_string(),
|
||||
signed_headers: signed_headers.to_string(),
|
||||
signature: signature.to_string(),
|
||||
content_sha256: content_sha256.to_string(),
|
||||
|
@ -304,3 +274,51 @@ fn canonical_query_string(uri: &hyper::Uri) -> String {
|
|||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_date(date: &str) -> Result<DateTime<Utc>, Error> {
|
||||
let date: NaiveDateTime =
|
||||
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
||||
Ok(DateTime::from_utc(date, Utc))
|
||||
}
|
||||
|
||||
pub async fn verify_v4(
|
||||
garage: &Garage,
|
||||
credential: &str,
|
||||
date: &DateTime<Utc>,
|
||||
signature: &str,
|
||||
payload: &[u8],
|
||||
) -> Result<Key, Error> {
|
||||
let (key_id, scope) = parse_credential(credential)?;
|
||||
|
||||
let scope_expected = format!(
|
||||
"{}/{}/s3/aws4_request",
|
||||
date.format(SHORT_DATE),
|
||||
garage.config.s3_api.s3_region
|
||||
);
|
||||
if scope != scope_expected {
|
||||
return Err(Error::AuthorizationHeaderMalformed(scope.to_string()));
|
||||
}
|
||||
|
||||
let key = garage
|
||||
.key_table
|
||||
.get(&EmptyKey, &key_id)
|
||||
.await?
|
||||
.filter(|k| !k.state.is_deleted())
|
||||
.ok_or_else(|| Error::Forbidden(format!("No such key: {}", &key_id)))?;
|
||||
let key_p = key.params().unwrap();
|
||||
|
||||
let mut hmac = signing_hmac(
|
||||
date,
|
||||
&key_p.secret_key,
|
||||
&garage.config.s3_api.s3_region,
|
||||
"s3",
|
||||
)
|
||||
.ok_or_internal_error("Unable to build signing HMAC")?;
|
||||
hmac.update(payload);
|
||||
let our_signature = hex::encode(hmac.finalize().into_bytes());
|
||||
if signature != our_signature {
|
||||
return Err(Error::Forbidden("Invalid signature".to_string()));
|
||||
}
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
|
|
@ -8,10 +8,16 @@ description = "Garage, an S3-compatible distributed object store for self-hosted
|
|||
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
|
||||
readme = "../../README.md"
|
||||
|
||||
autotests = false
|
||||
|
||||
[[bin]]
|
||||
name = "garage"
|
||||
path = "main.rs"
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
path = "tests/lib.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
@ -45,3 +51,9 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi
|
|||
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
aws-sdk-s3 = "0.6"
|
||||
http = "0.2"
|
||||
|
||||
static_init = "1.0"
|
||||
|
|
22
src/garage/tests/common/client.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use aws_sdk_s3::{Client, Config, Credentials, Endpoint};
|
||||
|
||||
use super::garage::Instance;
|
||||
|
||||
pub fn build_client(instance: &Instance) -> Client {
|
||||
let credentials = Credentials::new(
|
||||
&instance.key.id,
|
||||
&instance.key.secret,
|
||||
None,
|
||||
None,
|
||||
"garage-integ-test",
|
||||
);
|
||||
let endpoint = Endpoint::immutable(instance.uri());
|
||||
|
||||
let config = Config::builder()
|
||||
.region(super::REGION)
|
||||
.credentials_provider(credentials)
|
||||
.endpoint_resolver(endpoint)
|
||||
.build();
|
||||
|
||||
Client::from_conf(config)
|
||||
}
|
3
src/garage/tests/common/ext/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub use process::*;
|
||||
|
||||
mod process;
|
55
src/garage/tests/common/ext/process.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use std::process;
|
||||
|
||||
pub trait CommandExt {
|
||||
fn quiet(&mut self) -> &mut Self;
|
||||
|
||||
fn expect_success_status(&mut self, msg: &str) -> process::ExitStatus;
|
||||
fn expect_success_output(&mut self, msg: &str) -> process::Output;
|
||||
}
|
||||
|
||||
impl CommandExt for process::Command {
|
||||
fn quiet(&mut self) -> &mut Self {
|
||||
self.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
}
|
||||
|
||||
fn expect_success_status(&mut self, msg: &str) -> process::ExitStatus {
|
||||
let status = self.status().expect(msg);
|
||||
status.expect_success(msg);
|
||||
status
|
||||
}
|
||||
fn expect_success_output(&mut self, msg: &str) -> process::Output {
|
||||
let output = self.output().expect(msg);
|
||||
output.expect_success(msg);
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OutputExt {
|
||||
fn expect_success(&self, msg: &str);
|
||||
}
|
||||
|
||||
impl OutputExt for process::Output {
|
||||
fn expect_success(&self, msg: &str) {
|
||||
self.status.expect_success(msg)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExitStatusExt {
|
||||
fn expect_success(&self, msg: &str);
|
||||
}
|
||||
|
||||
impl ExitStatusExt for process::ExitStatus {
|
||||
fn expect_success(&self, msg: &str) {
|
||||
if !self.success() {
|
||||
match self.code() {
|
||||
Some(code) => panic!(
|
||||
"Command exited with code {code}: {msg}",
|
||||
code = code,
|
||||
msg = msg
|
||||
),
|
||||
None => panic!("Command exited with signal: {msg}", msg = msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
220
src/garage/tests/common/garage.rs
Normal file
|
@ -0,0 +1,220 @@
|
|||
use std::mem::MaybeUninit;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
use std::sync::Once;
|
||||
|
||||
use super::ext::*;
|
||||
|
||||
// https://xkcd.com/221/
|
||||
const DEFAULT_PORT: u16 = 49995;
|
||||
|
||||
static GARAGE_TEST_SECRET: &str =
|
||||
"c3ea8cb80333d04e208d136698b1a01ae370d463f0d435ab2177510b3478bf44";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Key {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
pub struct Instance {
|
||||
process: process::Child,
|
||||
pub path: PathBuf,
|
||||
pub key: Key,
|
||||
pub api_port: u16,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
fn new() -> Instance {
|
||||
use std::{env, fs};
|
||||
|
||||
let port = env::var("GARAGE_TEST_INTEGRATION_PORT")
|
||||
.map(|value| value.parse().expect("Invalid port provided"))
|
||||
.ok()
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
|
||||
let path = env::var("GARAGE_TEST_INTEGRATION_PATH")
|
||||
.map(PathBuf::from)
|
||||
.ok()
|
||||
.unwrap_or_else(|| env::temp_dir().join(format!("garage-integ-test-{}", port)));
|
||||
|
||||
// Clean test runtime directory
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(&path).expect("Could not clean test runtime directory");
|
||||
}
|
||||
fs::create_dir(&path).expect("Could not create test runtime directory");
|
||||
|
||||
let config = format!(
|
||||
r#"
|
||||
metadata_dir = "{path}/meta"
|
||||
data_dir = "{path}/data"
|
||||
|
||||
replication_mode = "1"
|
||||
|
||||
rpc_bind_addr = "127.0.0.1:{rpc_port}"
|
||||
rpc_public_addr = "127.0.0.1:{rpc_port}"
|
||||
rpc_secret = "{secret}"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "{region}"
|
||||
api_bind_addr = "127.0.0.1:{api_port}"
|
||||
root_domain = ".s3.garage"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "127.0.0.1:{web_port}"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
"#,
|
||||
path = path.display(),
|
||||
secret = GARAGE_TEST_SECRET,
|
||||
region = super::REGION,
|
||||
api_port = port,
|
||||
rpc_port = port + 1,
|
||||
web_port = port + 2,
|
||||
);
|
||||
fs::write(path.join("config.toml"), config).expect("Could not write garage config file");
|
||||
|
||||
let stdout =
|
||||
fs::File::create(path.join("stdout.log")).expect("Could not create stdout logfile");
|
||||
let stderr =
|
||||
fs::File::create(path.join("stderr.log")).expect("Could not create stderr logfile");
|
||||
|
||||
let child = command(&path.join("config.toml"))
|
||||
.arg("server")
|
||||
.stdout(stdout)
|
||||
.stderr(stderr)
|
||||
.env("RUST_LOG", "garage=info,garage_api=debug")
|
||||
.spawn()
|
||||
.expect("Could not start garage");
|
||||
|
||||
Instance {
|
||||
process: child,
|
||||
path,
|
||||
key: Key::default(),
|
||||
api_port: port,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self) {
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
// Wait for node to be ready
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
self.setup_layout();
|
||||
|
||||
self.key = self.new_key("garage_test");
|
||||
}
|
||||
|
||||
fn setup_layout(&self) {
|
||||
let node_id = self.node_id();
|
||||
let node_short_id = &node_id[..64];
|
||||
|
||||
self.command()
|
||||
.args(["layout", "assign"])
|
||||
.arg(node_short_id)
|
||||
.args(["-c", "1", "-z", "unzonned"])
|
||||
.quiet()
|
||||
.expect_success_status("Could not assign garage node layout");
|
||||
self.command()
|
||||
.args(["layout", "apply"])
|
||||
.args(["--version", "1"])
|
||||
.quiet()
|
||||
.expect_success_status("Could not apply garage node layout");
|
||||
}
|
||||
|
||||
fn terminate(&mut self) {
|
||||
// TODO: Terminate "gracefully" the process with SIGTERM instead of directly SIGKILL it.
|
||||
self.process
|
||||
.kill()
|
||||
.expect("Could not terminate garage process");
|
||||
}
|
||||
|
||||
pub fn command(&self) -> process::Command {
|
||||
command(&self.path.join("config.toml"))
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> String {
|
||||
let output = self
|
||||
.command()
|
||||
.args(["node", "id"])
|
||||
.expect_success_output("Could not get node ID");
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> http::Uri {
|
||||
format!("http://127.0.0.1:{api_port}", api_port = self.api_port)
|
||||
.parse()
|
||||
.expect("Could not build garage endpoint URI")
|
||||
}
|
||||
|
||||
pub fn new_key(&self, name: &str) -> Key {
|
||||
let mut key = Key::default();
|
||||
|
||||
let output = self
|
||||
.command()
|
||||
.args(["key", "new"])
|
||||
.args(["--name", name])
|
||||
.expect_success_output("Could not create key");
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Some(key_id) = line.strip_prefix("Key ID: ") {
|
||||
key.id = key_id.to_owned();
|
||||
continue;
|
||||
}
|
||||
if let Some(key_secret) = line.strip_prefix("Secret key: ") {
|
||||
key.secret = key_secret.to_owned();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
assert!(!key.id.is_empty(), "Invalid key: Key ID is empty");
|
||||
assert!(!key.secret.is_empty(), "Invalid key: Key secret is empty");
|
||||
|
||||
Key {
|
||||
name: name.to_owned(),
|
||||
..key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mut INSTANCE: MaybeUninit<Instance> = MaybeUninit::uninit();
|
||||
static INSTANCE_INIT: Once = Once::new();
|
||||
|
||||
#[static_init::destructor]
|
||||
extern "C" fn terminate_instance() {
|
||||
if INSTANCE_INIT.is_completed() {
|
||||
// This block is sound as it depends on `INSTANCE_INIT` being completed, meaning `INSTANCE`
|
||||
// is actually initialized.
|
||||
unsafe {
|
||||
INSTANCE.assume_init_mut().terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static Instance {
|
||||
INSTANCE_INIT.call_once(|| unsafe {
|
||||
let mut instance = Instance::new();
|
||||
instance.setup();
|
||||
|
||||
INSTANCE.write(instance);
|
||||
});
|
||||
|
||||
// This block is sound as it depends on `INSTANCE_INIT` being completed by calling `call_once` (blocking),
|
||||
// meaning `INSTANCE` is actually initialized.
|
||||
unsafe { INSTANCE.assume_init_ref() }
|
||||
}
|
||||
|
||||
pub fn command(config_path: &Path) -> process::Command {
|
||||
use std::env;
|
||||
|
||||
let mut command = process::Command::new(
|
||||
env::var("GARAGE_TEST_INTEGRATION_EXE")
|
||||
.unwrap_or_else(|_| env!("CARGO_BIN_EXE_garage").to_owned()),
|
||||
);
|
||||
|
||||
command.arg("-c").arg(config_path);
|
||||
|
||||
command
|
||||
}
|
11
src/garage/tests/common/macros.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
macro_rules! assert_bytes_eq {
|
||||
($stream:expr, $bytes:expr) => {
|
||||
let data = $stream
|
||||
.collect()
|
||||
.await
|
||||
.expect("Error reading data")
|
||||
.into_bytes();
|
||||
|
||||
assert_eq!(data.as_ref(), $bytes);
|
||||
};
|
||||
}
|
52
src/garage/tests/common/mod.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use aws_sdk_s3::{Client, Region};
|
||||
use ext::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod client;
|
||||
pub mod ext;
|
||||
pub mod garage;
|
||||
|
||||
const REGION: Region = Region::from_static("garage-integ-test");
|
||||
|
||||
pub struct Context {
|
||||
pub garage: &'static garage::Instance,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
fn new() -> Self {
|
||||
let garage = garage::instance();
|
||||
let client = client::build_client(garage);
|
||||
|
||||
Context { garage, client }
|
||||
}
|
||||
|
||||
/// Create an unique bucket with a random suffix.
|
||||
///
|
||||
/// Return the created bucket full name.
|
||||
pub fn create_bucket(&self, name: &str) -> String {
|
||||
let bucket_name = name.to_owned();
|
||||
|
||||
self.garage
|
||||
.command()
|
||||
.args(["bucket", "create", &bucket_name])
|
||||
.quiet()
|
||||
.expect_success_status("Could not create bucket");
|
||||
self.garage
|
||||
.command()
|
||||
.args(["bucket", "allow"])
|
||||
.args(["--owner", "--read", "--write"])
|
||||
.arg(&bucket_name)
|
||||
.args(["--key", &self.garage.key.name])
|
||||
.quiet()
|
||||
.expect_success_status("Could not allow key for bucket");
|
||||
|
||||
bucket_name
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context() -> Context {
|
||||
Context::new()
|
||||
}
|
4
src/garage/tests/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
mod simple;
|
31
src/garage/tests/simple.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::common;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simple() {
|
||||
use aws_sdk_s3::ByteStream;
|
||||
|
||||
let ctx = common::context();
|
||||
let bucket = ctx.create_bucket("test-simple");
|
||||
|
||||
let data = ByteStream::from_static(b"Hello world!");
|
||||
|
||||
ctx.client
|
||||
.put_object()
|
||||
.bucket(&bucket)
|
||||
.key("test")
|
||||
.body(data)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key("test")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_bytes_eq!(res.body, b"Hello world!");
|
||||
}
|
|
@ -130,22 +130,31 @@ where
|
|||
let tree_key = self.tree_key(update.partition_key(), update.sort_key());
|
||||
|
||||
let changed = (&self.store, &self.merkle_todo).transaction(|(store, mkl_todo)| {
|
||||
let (old_entry, new_entry) = match store.get(&tree_key)? {
|
||||
Some(prev_bytes) => {
|
||||
let (old_entry, old_bytes, new_entry) = match store.get(&tree_key)? {
|
||||
Some(old_bytes) => {
|
||||
let old_entry = self
|
||||
.decode_entry(&prev_bytes)
|
||||
.decode_entry(&old_bytes)
|
||||
.map_err(sled::transaction::ConflictableTransactionError::Abort)?;
|
||||
let mut new_entry = old_entry.clone();
|
||||
new_entry.merge(&update);
|
||||
(Some(old_entry), new_entry)
|
||||
(Some(old_entry), Some(old_bytes), new_entry)
|
||||
}
|
||||
None => (None, update.clone()),
|
||||
None => (None, None, update.clone()),
|
||||
};
|
||||
|
||||
if Some(&new_entry) != old_entry.as_ref() {
|
||||
let new_bytes = rmp_to_vec_all_named(&new_entry)
|
||||
.map_err(Error::RmpEncode)
|
||||
.map_err(sled::transaction::ConflictableTransactionError::Abort)?;
|
||||
// Scenario 1: the value changed, so of course there is a change
|
||||
let value_changed = Some(&new_entry) != old_entry.as_ref();
|
||||
|
||||
// Scenario 2: the value didn't change but due to a migration in the
|
||||
// data format, the messagepack encoding changed. In this case
|
||||
// we have to write the migrated value in the table and update
|
||||
// the associated Merkle tree entry.
|
||||
let new_bytes = rmp_to_vec_all_named(&new_entry)
|
||||
.map_err(Error::RmpEncode)
|
||||
.map_err(sled::transaction::ConflictableTransactionError::Abort)?;
|
||||
let encoding_changed = Some(&new_bytes[..]) != old_bytes.as_ref().map(|x| &x[..]);
|
||||
|
||||
if value_changed || encoding_changed {
|
||||
let new_bytes_hash = blake2sum(&new_bytes[..]);
|
||||
mkl_todo.insert(tree_key.clone(), new_bytes_hash.as_slice())?;
|
||||
store.insert(tree_key.clone(), new_bytes)?;
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::error::*;
|
|||
|
||||
use garage_api::error::{Error as ApiError, OkOrBadRequest, OkOrInternalError};
|
||||
use garage_api::helpers::{authority_to_host, host_to_bucket};
|
||||
use garage_api::s3_cors::{add_cors_headers, find_matching_cors_rule, handle_options};
|
||||
use garage_api::s3_cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket};
|
||||
use garage_api::s3_get::{handle_get, handle_head};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
|
@ -133,7 +133,7 @@ async fn serve_file(garage: Arc<Garage>, req: &Request<Body>) -> Result<Response
|
|||
);
|
||||
|
||||
let ret_doc = match *req.method() {
|
||||
Method::OPTIONS => handle_options(req, &bucket).await,
|
||||
Method::OPTIONS => handle_options_for_bucket(req, &bucket),
|
||||
Method::HEAD => handle_head(garage.clone(), req, bucket_id, &key, None).await,
|
||||
Method::GET => handle_get(garage.clone(), req, bucket_id, &key, None).await,
|
||||
_ => Err(ApiError::BadRequest("HTTP method not supported".into())),
|
||||
|
|