Compare commits

...
Sign in to create a new pull request.

33 commits

Author SHA1 Message Date
d3226bfa91 Merge pull request 'remove uses of #[async_trait]' (#952) from remove-async-trait into main
Reviewed-on: Deuxfleurs/garage#952
2025-02-05 19:52:00 +00:00
af67626ab2 remove async_trait for TableRepair 2025-02-05 20:45:07 +01:00
5475da8ea8 remove async_trait used in generic_server.rs 2025-02-05 20:31:34 +01:00
620dc58560 remove async_trait for traits declared in garage_net 2025-02-05 20:22:16 +01:00
47e87c8739 Merge pull request 'upgrade Rust compiler and Cargo dependencies' (#951) from nix-crane into main
Reviewed-on: Deuxfleurs/garage#951
2025-02-03 17:49:00 +00:00
34599bff51 update all Cargo dependencies except AWS crates and their dependencies 2025-02-03 17:46:54 +01:00
ec1a475923 build with rust 1.82.0 2025-02-03 17:46:48 +01:00
b9df2d1ad1 Merge pull request 'compile with crane' (#950) from nix-crane into main
Reviewed-on: Deuxfleurs/garage#950
2025-02-03 15:54:54 +00:00
390a5d97fe nix, ci: build with Crane
This removes our dependency on cargo2nix, which was causing us some
issues. Whereas cargo2nix creates one Nix derivation per crate, Crane
uses only two derivations:

1. Build dependencies only
2. Build the final binary

This means that during the second step, no caching can be done. For
instance, if we do a change in garage_model, we need to recompile all of
the Garage crates including those that do not depend on garage_model.
On the upside, this allows all of the Garage crates to be built at once
using cargo build logic, which is optimized for high parallelism and
better pipelining between all of the steps of the build. All in all,
this makes most builds faster than cargo2nix.

A few other changes have been made to the build scripts and CI:

- Unit tests are now run within a Nix derivation. In fact, we have
  different derivations to run the tests using LMDB and Sqlite as
  metadata db engines.

- For debug builds, most CI steps now run in parallel (with the notable
  exception of the smoke test that runs after the build, which is
  inevitable).

- We no longer pass the GIT_VERSION argument when building debug builds
  and running the tests. This means that dev binaries and test
  binaries don't know the exact version of Garage they are from. That
  shouldn't be an issue in most cases.

- The not-dynamic.sh scripts has been fixed to fail if the file does not
  exist.
2025-02-03 16:39:50 +01:00
4dc2bc337f Merge pull request 'woodpecker: use parallel nix-build in debug builds' (#949) from nix-parallel into main
Reviewed-on: Deuxfleurs/garage#949
2025-02-01 18:58:15 +00:00
5dd2791981 woodpecker: use parallel nix-build in debug builds 2025-02-01 19:48:01 +01:00
d601f31186 Merge pull request 'split garage_api in garage_api_{common,s3,k2v,admin}' (#947) from split-garage-api into main
Reviewed-on: Deuxfleurs/garage#947
2025-02-01 17:48:25 +00:00
e4de7bdfd5 fix ci for more test crates 2025-01-31 19:21:36 +01:00
d18c5ad0ff fix tests 2025-01-31 19:12:51 +01:00
3d5e9a027e cargo defs: simplify and fix descriptions 2025-01-31 18:54:29 +01:00
f4ca7758b4 update cargo.nix 2025-01-31 18:48:07 +01:00
4563313f87 use cargo-shear to remove many unused dependencies between crates 2025-01-31 18:47:30 +01:00
afa28706e5 split s3/cors.rs into also common/cors.rs 2025-01-31 18:42:14 +01:00
84f1db91c4 fix things up 2025-01-31 18:34:57 +01:00
9fa20d45be wip: split garage_api into garage_api_{common,s3,k2v,admin} 2025-01-31 18:18:29 +01:00
9330fd79d3 Merge pull request 'table::insert_many: avoid failure with zero items (fix #915)' (#946) from fix-915 into main
Reviewed-on: Deuxfleurs/garage#946
2025-01-31 13:10:54 +00:00
83f6928ff7 table::insert_many: avoid failure with zero items (fix #915) 2025-01-30 18:06:47 +01:00
ab71544499 Merge pull request 'api: better handling of helper errors to distinguish error codes' (#942) from fix-getkeyinfo-404 into main
Reviewed-on: Deuxfleurs/garage#942
2025-01-29 18:25:44 +00:00
991edbe02c Merge pull request 'Update doc/book/connect/repositories.md' (#941) from yatesco/garage:main into main
Reviewed-on: Deuxfleurs/garage#941
2025-01-29 18:18:59 +00:00
9f3c7c3720 api: better handling of helper errors to distinguish error codes 2025-01-29 19:14:34 +01:00
bfde9152b8 Update doc/book/operations/multi-hdd.md
trivial spelling mistake
2025-01-29 13:40:41 +00:00
7bb042f0b7 Update doc/book/connect/repositories.md
trivial spelling mistake
2025-01-29 13:34:35 +00:00
a1d081ee84 Merge pull request 's3 api: make x-amz-meta-* headers lowercase (fix #844)' (#938) from fix-844 into main
Reviewed-on: Deuxfleurs/garage#938
2025-01-27 19:32:19 +00:00
e8fa89e834 s3 api: make x-amz-meta-* headers lowercase (fix #844) 2025-01-27 19:58:06 +01:00
beedc9fd11 Merge pull request 'snapshot: sqlite: use a subdirectory for consistency with LMDB' (#932) from baptiste/garage:snapshot_consistency_sqlite into main
Reviewed-on: Deuxfleurs/garage#932
2025-01-27 18:50:11 +00:00
d4e3e60920 Merge pull request 'update nix crate to 0.29 and libc to 0.2.169' (#931) from neuschaefer/garage:nix into main
Reviewed-on: Deuxfleurs/garage#931
2025-01-27 18:09:51 +00:00
74a1b49b13 Update Cargo.nix 2025-01-27 18:37:05 +01:00
23d57b89dc update nix crate to 0.29 and libc to 0.2.169 2025-01-27 18:37:05 +01:00
98 changed files with 2001 additions and 8631 deletions

View file

@ -1,3 +0,0 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View file

@ -16,31 +16,21 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA} - nix-build -j4 --attr flakePackages.dev
- name: unit + func tests - name: unit + func tests (lmdb)
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
environment:
GARAGE_TEST_INTEGRATION_EXE: result-bin/bin/garage
GARAGE_TEST_INTEGRATION_PATH: tmp-garage-integration
commands: commands:
- nix-build --no-build-output --attr test.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA} - nix-build -j4 --attr flakePackages.tests-lmdb
- ./result/bin/garage_db-*
- ./result/bin/garage_api-* - name: unit + func tests (sqlite)
- ./result/bin/garage_model-* image: nixpkgs/nix:nixos-22.05
- ./result/bin/garage_rpc-* commands:
- ./result/bin/garage_table-* - nix-build -j4 --attr flakePackages.tests-sqlite
- ./result/bin/garage_util-*
- ./result/bin/garage_web-*
- ./result/bin/garage-*
- GARAGE_TEST_INTEGRATION_DB_ENGINE=lmdb ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
- nix-shell --attr ci --run "killall -9 garage" || true
- GARAGE_TEST_INTEGRATION_DB_ENGINE=sqlite ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
- rm result
- rm -rv tmp-garage-integration
- name: integration tests - name: integration tests
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA} - nix-build -j4 --attr flakePackages.dev
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
depends_on: [ build ]

View file

@ -18,12 +18,12 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.${ARCH}.release --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA} - nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- name: check is static binary - name: check is static binary
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-shell --attr ci --run "./script/not-dynamic.sh result-bin/bin/garage" - nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
- name: integration tests - name: integration tests
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05

1683
Cargo.lock generated

File diff suppressed because it is too large Load diff

7102
Cargo.nix

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,10 @@ members = [
"src/table", "src/table",
"src/block", "src/block",
"src/model", "src/model",
"src/api", "src/api/common",
"src/api/s3",
"src/api/k2v",
"src/api/admin",
"src/web", "src/web",
"src/garage", "src/garage",
"src/k2v-client", "src/k2v-client",
@ -21,7 +24,10 @@ default-members = ["src/garage"]
# Internal Garage crates # Internal Garage crates
format_table = { version = "0.1.1", path = "src/format-table" } format_table = { version = "0.1.1", path = "src/format-table" }
garage_api = { version = "1.0.1", path = "src/api" } garage_api_common = { version = "1.0.1", path = "src/api/common" }
garage_api_admin = { version = "1.0.1", path = "src/api/admin" }
garage_api_s3 = { version = "1.0.1", path = "src/api/s3" }
garage_api_k2v = { version = "1.0.1", path = "src/api/k2v" }
garage_block = { version = "1.0.1", path = "src/block" } garage_block = { version = "1.0.1", path = "src/block" }
garage_db = { version = "1.0.1", path = "src/db", default-features = false } garage_db = { version = "1.0.1", path = "src/db", default-features = false }
garage_model = { version = "1.0.1", path = "src/model", default-features = false } garage_model = { version = "1.0.1", path = "src/model", default-features = false }
@ -46,7 +52,6 @@ chrono = "0.4"
crc32fast = "1.4" crc32fast = "1.4"
crc32c = "0.6" crc32c = "0.6"
crypto-common = "0.1" crypto-common = "0.1"
digest = "0.10"
err-derive = "0.3" err-derive = "0.3"
gethostname = "0.4" gethostname = "0.4"
git-version = "0.3.4" git-version = "0.3.4"
@ -59,7 +64,7 @@ ipnet = "2.9.0"
lazy_static = "1.4" lazy_static = "1.4"
md-5 = "0.10" md-5 = "0.10"
mktemp = "0.5" mktemp = "0.5"
nix = { version = "0.27", default-features = false, features = ["fs"] } nix = { version = "0.29", default-features = false, features = ["fs"] }
nom = "7.1" nom = "7.1"
parse_duration = "2.1" parse_duration = "2.1"
pin-project = "1.0.12" pin-project = "1.0.12"
@ -136,8 +141,6 @@ thiserror = "1.0"
assert-json-diff = "2.0" assert-json-diff = "2.0"
rustc_version = "0.4.0" rustc_version = "0.4.0"
static_init = "1.0" static_init = "1.0"
aws-config = "1.1.4"
aws-sdk-config = "1.13" aws-sdk-config = "1.13"
aws-sdk-s3 = "1.14" aws-sdk-s3 = "1.14"

View file

@ -3,5 +3,5 @@ FROM scratch
ENV RUST_BACKTRACE=1 ENV RUST_BACKTRACE=1
ENV RUST_LOG=garage=info ENV RUST_LOG=garage=info
COPY result-bin/bin/garage / COPY result/bin/garage /
CMD [ "/garage", "server"] CMD [ "/garage", "server"]

View file

@ -1,13 +1,10 @@
.PHONY: doc all release shell run1 run2 run3 .PHONY: doc all run1 run2 run3
all: all:
clear; cargo build clear
cargo build \
release: --config 'target.x86_64-unknown-linux-gnu.linker="clang"' \
nix-build --attr pkgs.amd64.release --no-build-output --config 'target.x86_64-unknown-linux-gnu.rustflags=["-C", "link-arg=-fuse-ld=mold"]' \
shell:
nix-shell
# ---- # ----

View file

@ -3,46 +3,22 @@
with import ./nix/common.nix; with import ./nix/common.nix;
let let
pkgs = import pkgsSrc { }; pkgs = import nixpkgs { };
compile = import ./nix/compile.nix; compile = import ./nix/compile.nix;
build_debug_and_release = (target: { build_release = target: (compile {
debug = (compile { inherit target system git_version nixpkgs;
inherit system target git_version pkgsSrc cargo2nixOverlay; crane = flake.inputs.crane;
release = false; rust-overlay = flake.inputs.rust-overlay;
}).workspace.garage { compileMode = "build"; };
release = (compile {
inherit system target git_version pkgsSrc cargo2nixOverlay;
release = true; release = true;
}).workspace.garage { compileMode = "build"; }; }).garage;
});
test = (rustPkgs:
pkgs.symlinkJoin {
name = "garage-tests";
paths =
builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; })
(builtins.attrNames rustPkgs.workspace);
});
in { in {
pkgs = { releasePackages = {
amd64 = build_debug_and_release "x86_64-unknown-linux-musl"; amd64 = build_release "x86_64-unknown-linux-musl";
i386 = build_debug_and_release "i686-unknown-linux-musl"; i386 = build_release "i686-unknown-linux-musl";
arm64 = build_debug_and_release "aarch64-unknown-linux-musl"; arm64 = build_release "aarch64-unknown-linux-musl";
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf"; arm = build_release "armv6l-unknown-linux-musleabihf";
};
test = {
amd64 = test (compile {
inherit system git_version pkgsSrc cargo2nixOverlay;
target = "x86_64-unknown-linux-musl";
features = [
"garage/bundled-libs"
"garage/k2v"
"garage/lmdb"
"garage/sqlite"
];
});
}; };
flakePackages = flake.packages.${system};
} }

View file

@ -17,7 +17,7 @@ Garage can also help you serve this content.
## Gitea ## Gitea
You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachements. You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachments.
You can configure a different target for each data type (check `[lfs]` and `[attachment]` sections of the Gitea documentation) and you can provide a default one through the `[storage]` section. You can configure a different target for each data type (check `[lfs]` and `[attachment]` sections of the Gitea documentation) and you can provide a default one through the `[storage]` section.
Let's start by creating a key and a bucket (your key id and secret will be needed later, keep them somewhere): Let's start by creating a key and a bucket (your key id and secret will be needed later, keep them somewhere):

View file

@ -21,14 +21,14 @@ data_dir = [
``` ```
Garage will automatically balance all blocks stored by the node Garage will automatically balance all blocks stored by the node
among the different specified directories, proportionnally to the among the different specified directories, proportionally to the
specified capacities. specified capacities.
## Updating the list of storage locations ## Updating the list of storage locations
If you add new storage locations to your `data_dir`, If you add new storage locations to your `data_dir`,
Garage will not rebalance existing data between storage locations. Garage will not rebalance existing data between storage locations.
Newly written blocks will be balanced proportionnally to the specified capacities, Newly written blocks will be balanced proportionally to the specified capacities,
and existing data may be moved between drives to improve balancing, and existing data may be moved between drives to improve balancing,
but only opportunistically when a data block is re-written (e.g. an object but only opportunistically when a data block is re-written (e.g. an object
is re-uploaded, or an object with a duplicate block is uploaded). is re-uploaded, or an object with a duplicate block is uploaded).

70
flake.lock generated
View file

@ -1,28 +1,17 @@
{ {
"nodes": { "nodes": {
"cargo2nix": { "crane": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": { "locked": {
"lastModified": 1705129117, "lastModified": 1737689766,
"narHash": "sha256-LgdDHibvimzYhxBK3kxCk2gAL7k4Hyigl5KI0X9cijA=", "narHash": "sha256-ivVXYaYlShxYoKfSo5+y5930qMKKJ8CLcAoIBPQfJ6s=",
"owner": "cargo2nix", "owner": "ipetkov",
"repo": "cargo2nix", "repo": "crane",
"rev": "ae19a9e1f8f0880c088ea155ab66cee1fa001f59", "rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "cargo2nix", "owner": "ipetkov",
"repo": "cargo2nix", "repo": "crane",
"rev": "ae19a9e1f8f0880c088ea155ab66cee1fa001f59",
"type": "github" "type": "github"
} }
}, },
@ -42,12 +31,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1731533236,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -74,34 +66,46 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"cargo2nix": "cargo2nix", "crane": "crane",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": [ "flake-utils": "flake-utils",
"cargo2nix", "nixpkgs": "nixpkgs",
"flake-utils" "rust-overlay": "rust-overlay"
],
"nixpkgs": "nixpkgs"
} }
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"cargo2nix",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1736649126, "lastModified": 1738549608,
"narHash": "sha256-XCw5sv/ePsroqiF3lJM6Y2X9EhPdHeE47gr3Q8b0UQw=", "narHash": "sha256-GdyT9QEUSx5k/n8kILuNy83vxxdyUfJ8jL5mMpQZWfw=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "162ab0edc2936508470199b2e8e6c444a2535019", "rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "162ab0edc2936508470199b2e8e6c444a2535019", "rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github" "type": "github"
} }
} }

100
flake.nix
View file

@ -2,92 +2,84 @@
description = description =
"Garage, an S3-compatible distributed object store for self-hosted deployments"; "Garage, an S3-compatible distributed object store for self-hosted deployments";
# Nixpkgs 24.11 as of 2025-01-12 has rustc v1.82 # Nixpkgs 24.11 as of 2025-01-12
inputs.nixpkgs.url = inputs.nixpkgs.url =
"github:NixOS/nixpkgs/7c4869c47090dd7f9f1bdfb49a22aea026996815"; "github:NixOS/nixpkgs/7c4869c47090dd7f9f1bdfb49a22aea026996815";
inputs.flake-compat.url = "github:nix-community/flake-compat"; # Rust overlay as of 2025-02-03
inputs.cargo2nix = {
# As of 2022-10-18: two small patches over unstable branch, one for clippy and one to fix feature detection
#url = "github:Alexis211/cargo2nix/a7a61179b66054904ef6a195d8da736eaaa06c36";
# As of 2023-04-25:
# - my two patches were merged into unstable (one for clippy and one to "fix" feature detection)
# - rustc v1.66
# url = "github:cargo2nix/cargo2nix/8fb57a670f7993bfc24099c33eb9c5abb51f29a2";
# Mainline cargo2nix as of of 2025-01-12 (branch release-0.11.0)
url = "github:cargo2nix/cargo2nix/ae19a9e1f8f0880c088ea155ab66cee1fa001f59";
# Rust overlay as of 2025-01-12
inputs.rust-overlay.url = inputs.rust-overlay.url =
"github:oxalica/rust-overlay/162ab0edc2936508470199b2e8e6c444a2535019"; "github:oxalica/rust-overlay/35c6f8c4352f995ecd53896200769f80a3e8f22d";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs"; inputs.crane.url = "github:ipetkov/crane";
inputs.flake-compat.follows = "flake-compat";
};
inputs.flake-utils.follows = "cargo2nix/flake-utils"; inputs.flake-compat.url = "github:nix-community/flake-compat";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, cargo2nix, flake-utils, ... }: outputs = { self, nixpkgs, flake-utils, crane, rust-overlay, ... }:
let let
git_version = self.lastModifiedDate;
compile = import ./nix/compile.nix; compile = import ./nix/compile.nix;
in in
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
packageFor = target: release: (compile {
inherit system target nixpkgs crane rust-overlay release;
}).garage;
testWith = extraTestEnv: (compile {
inherit system nixpkgs crane rust-overlay extraTestEnv;
release = false;
}).garage-test;
in in
{ {
packages = packages = {
let
packageFor = target: (compile {
inherit system git_version target;
pkgsSrc = nixpkgs;
cargo2nixOverlay = cargo2nix.overlays.default;
release = true;
}).workspace.garage { compileMode = "build"; };
in
{
# default = native release build # default = native release build
default = packageFor null; default = packageFor null true;
# other = cross-compiled, statically-linked builds
amd64 = packageFor "x86_64-unknown-linux-musl"; # <arch> = cross-compiled, statically-linked release builds
i386 = packageFor "i686-unknown-linux-musl"; amd64 = packageFor "x86_64-unknown-linux-musl" true;
arm64 = packageFor "aarch64-unknown-linux-musl"; i386 = packageFor "i686-unknown-linux-musl" true;
arm = packageFor "armv6l-unknown-linux-musl"; arm64 = packageFor "aarch64-unknown-linux-musl" true;
arm = packageFor "armv6l-unknown-linux-musl" true;
# dev = native dev build
dev = packageFor null false;
# test = cargo test
tests = testWith {};
tests-lmdb = testWith {
GARAGE_TEST_INTEGRATION_DB_ENGINE = "lmdb";
};
tests-sqlite = testWith {
GARAGE_TEST_INTEGRATION_DB_ENGINE = "sqlite";
};
}; };
# ---- developpment shell, for making native builds only ---- # ---- developpment shell, for making native builds only ----
devShells = devShells =
let let
shellWithPackages = (packages: (compile { targets = compile {
inherit system git_version; inherit system nixpkgs crane rust-overlay;
pkgsSrc = nixpkgs; };
cargo2nixOverlay = cargo2nix.overlays.default;
}).workspaceShell { inherit packages; });
in in
{ {
default = shellWithPackages default = targets.devShell;
(with pkgs; [
rustfmt
clang
mold
]);
# import the full shell using `nix develop .#full` # import the full shell using `nix develop .#full`
full = shellWithPackages (with pkgs; [ full = pkgs.mkShell {
rustfmt buildInputs = with pkgs; [
rust-analyzer targets.toolchain
protobuf
clang clang
mold mold
# ---- extra packages for dev tasks ---- # ---- extra packages for dev tasks ----
rust-analyzer
cargo-audit cargo-audit
cargo-outdated cargo-outdated
cargo-machete cargo-machete
nixpkgs-fmt nixpkgs-fmt
]); ];
};
}; };
}); });
} }

View file

@ -2,7 +2,7 @@
with import ./common.nix; with import ./common.nix;
let let
pkgs = import pkgsSrc { }; pkgs = import nixpkgs { };
lib = pkgs.lib; lib = pkgs.lib;
/* Converts a key list and a value list to a set /* Converts a key list and a value list to a set

View file

@ -10,9 +10,9 @@ let
flake = (import flake-compat { system = builtins.currentSystem; src = ../.; }); flake = (import flake-compat { system = builtins.currentSystem; src = ../.; });
in in
rec {
pkgsSrc = flake.defaultNix.inputs.nixpkgs; {
cargo2nix = flake.defaultNix.inputs.cargo2nix; flake = flake.defaultNix;
cargo2nixOverlay = cargo2nix.overlays.default; nixpkgs = flake.defaultNix.inputs.nixpkgs;
devShells = builtins.getAttr builtins.currentSystem flake.defaultNix.devShells; devShells = flake.defaultNix.devShells.${builtins.currentSystem};
} }

View file

@ -1,83 +1,64 @@
{ system, target ? null, pkgsSrc, cargo2nixOverlay {
, release ? false, git_version ? null, features ? null, }: /* build inputs */
nixpkgs,
crane,
rust-overlay,
/* parameters */
system,
git_version ? null,
target ? null,
release ? false,
features ? null,
extraTestEnv ? {}
}:
let let
log = v: builtins.trace v v; log = v: builtins.trace v v;
# NixOS and Rust/Cargo triples do not match for ARM, fix it here.
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then
"arm-unknown-linux-musleabihf"
else
target;
rustTargetEnvMap = {
"x86_64-unknown-linux-musl" = "X86_64_UNKNOWN_LINUX_MUSL";
"aarch64-unknown-linux-musl" = "AARCH64_UNKNOWN_LINUX_MUSL";
"i686-unknown-linux-musl" = "I686_UNKNOWN_LINUX_MUSL";
"arm-unknown-linux-musleabihf" = "ARM_UNKNOWN_LINUX_MUSLEABIHF";
};
pkgsNative = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
pkgs = if target != null then pkgs = if target != null then
import pkgsSrc { import nixpkgs {
inherit system; inherit system;
crossSystem = { crossSystem = {
config = target; config = target;
isStatic = true; isStatic = true;
}; };
overlays = [ cargo2nixOverlay ]; overlays = [ (import rust-overlay) ];
} }
else else
import pkgsSrc { pkgsNative;
inherit system;
overlays = [ cargo2nixOverlay ];
};
toolchainOptions = { inherit (pkgs) lib stdenv;
rustVersion = "1.78.0";
extraRustComponents = [ "clippy" ];
};
/* Cargo2nix provides many overrides by default, you can take inspiration from them: toolchainFn = (p: p.rust-bin.stable."1.82.0".default.override {
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix targets = lib.optionals (target != null) [ rustTarget ];
extensions = [
You can have a complete list of the available options by looking at the overriden object, mkcrate: "rust-src"
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix "rustfmt"
*/
packageOverrides = pkgs:
pkgs.rustBuilder.overrides.all ++ [
/* [1] We need to alter Nix hardening to make static binaries: PIE,
Position Independent Executables seems to be supported only on amd64. Having
this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms.
Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for 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 at: 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.
[3] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library.
However the features to do so get activated for some reason (due to a bug in cargo2nix?),
so disable them manually here.
*/
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage";
overrideAttrs = drv:
(if git_version != null then {
# [2]
preConfigure = ''
${drv.preConfigure or ""}
export GIT_VERSION="${git_version}"
'';
} else
{ }) // {
# [1]
hardeningDisable = [ "pie" ];
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "libsodium-sys";
overrideArgs = old: {
features = [ ]; # [3]
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "zstd-sys";
overrideArgs = old: {
features = [ ]; # [3]
};
})
]; ];
});
craneLib = (crane.mkLib pkgs).overrideToolchain toolchainFn;
src = craneLib.cleanCargoSource ../.;
/* We ship some parts of the code disabled by default by putting them behind a flag. /* We ship some parts of the code disabled by default by putting them behind a flag.
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
@ -87,16 +68,15 @@ let
rootFeatures = if features != null then rootFeatures = if features != null then
features features
else else
([ "garage/bundled-libs" "garage/lmdb" "garage/sqlite" "garage/k2v" ] ++ (if release then [ ([ "bundled-libs" "lmdb" "sqlite" "k2v" ] ++ (lib.optionals release [
"garage/consul-discovery" "consul-discovery"
"garage/kubernetes-discovery" "kubernetes-discovery"
"garage/metrics" "metrics"
"garage/telemetry-otlp" "telemetry-otlp"
"garage/syslog" "syslog"
] else ]));
[ ]));
packageFun = import ../Cargo.nix; featuresStr = lib.concatStringsSep "," rootFeatures;
/* We compile fully static binaries with musl to simplify deployment on most systems. /* We compile fully static binaries with musl to simplify deployment on most systems.
When possible, we reactivate PIE hardening (see above). When possible, we reactivate PIE hardening (see above).
@ -107,12 +87,9 @@ let
For more information on static builds, please refer to Rust's RFC 1721. 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 https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
*/ */
codegenOptsMap = {
codegenOpts = { "x86_64-unknown-linux-musl" =
"armv6l-unknown-linux-musleabihf" = [ [ "target-feature=+crt-static" "link-arg=-static-pie" ];
"target-feature=+crt-static"
"link-arg=-static"
]; # compile as dynamic with static-pie
"aarch64-unknown-linux-musl" = [ "aarch64-unknown-linux-musl" = [
"target-feature=+crt-static" "target-feature=+crt-static"
"link-arg=-static" "link-arg=-static"
@ -121,18 +98,95 @@ let
"target-feature=+crt-static" "target-feature=+crt-static"
"link-arg=-static" "link-arg=-static"
]; # segfault with static-pie ]; # segfault with static-pie
"x86_64-unknown-linux-musl" = "armv6l-unknown-linux-musleabihf" = [
[ "target-feature=+crt-static" "link-arg=-static-pie" ]; "target-feature=+crt-static"
"link-arg=-static"
]; # compile as dynamic with static-pie
}; };
# NixOS and Rust/Cargo triples do not match for ARM, fix it here. codegenOpts = if target != null then codegenOptsMap.${target} else [
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then "link-arg=-fuse-ld=mold"
"arm-unknown-linux-musleabihf" ];
else
target;
in pkgs.rustBuilder.makePackageSet ({ commonArgs =
inherit release packageFun packageOverrides codegenOpts rootFeatures; {
target = rustTarget; inherit src;
workspaceSrc = pkgs.lib.cleanSource ../.; pname = "garage";
} // toolchainOptions) version = "dev";
strictDeps = true;
cargoExtraArgs = "--locked --features ${featuresStr}";
cargoTestExtraArgs = "--workspace";
nativeBuildInputs = [
pkgsNative.protobuf
pkgs.stdenv.cc
] ++ lib.optionals (target == null) [
pkgs.clang
pkgs.mold
];
CARGO_PROFILE = if release then "release" else "dev";
CARGO_BUILD_RUSTFLAGS =
lib.concatStringsSep
" "
(builtins.map (flag: "-C ${flag}") codegenOpts);
}
//
(if rustTarget != null then {
CARGO_BUILD_TARGET = rustTarget;
"CARGO_TARGET_${rustTargetEnvMap.${rustTarget}}_LINKER" = "${stdenv.cc.targetPrefix}cc";
HOST_CC = "${stdenv.cc.nativePrefix}cc";
TARGET_CC = "${stdenv.cc.targetPrefix}cc";
} else {
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "clang";
});
in rec {
toolchain = toolchainFn pkgs;
devShell = pkgs.mkShell {
buildInputs = [
toolchain
] ++ (with pkgs; [
protobuf
clang
mold
]);
};
# ---- building garage ----
garage-deps = craneLib.buildDepsOnly commonArgs;
garage = craneLib.buildPackage (commonArgs // {
cargoArtifacts = garage-deps;
doCheck = false;
} //
(if git_version != null then {
version = git_version;
GIT_VERSION = git_version;
} else {}));
# ---- testing garage ----
garage-test-bin = craneLib.cargoBuild (commonArgs // {
cargoArtifacts = garage-deps;
pname = "garage-tests";
CARGO_PROFILE = "test";
cargoExtraArgs = "${commonArgs.cargoExtraArgs} --tests --workspace";
doCheck = false;
});
garage-test = craneLib.cargoTest (commonArgs // {
cargoArtifacts = garage-test-bin;
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
pkgs.cacert
];
} // extraTestEnv);
}

View file

@ -7,7 +7,12 @@ if [ "$#" -ne 1 ]; then
exit 2 exit 2
fi fi
if file $1 | grep 'dynamically linked' 2>&1; then if [ ! -x "$1" ]; then
echo "[fail] $1 does not exist or is not an executable"
exit 1
fi
if file "$1" | grep 'dynamically linked' 2>&1; then
echo "[fail] $1 is dynamic" echo "[fail] $1 is dynamic"
exit 1 exit 1
fi fi

View file

@ -3,7 +3,7 @@
with import ./nix/common.nix; with import ./nix/common.nix;
let let
pkgs = import pkgsSrc { pkgs = import nixpkgs {
inherit system; inherit system;
}; };
winscp = (import ./nix/winscp.nix) pkgs; winscp = (import ./nix/winscp.nix) pkgs;
@ -39,7 +39,7 @@ in
--endpoint-url https://garage.deuxfleurs.fr \ --endpoint-url https://garage.deuxfleurs.fr \
--region garage \ --region garage \
s3 cp \ s3 cp \
./result-bin/bin/garage \ ./result/bin/garage \
s3://garagehq.deuxfleurs.fr/_releases/''${CI_COMMIT_TAG:-$CI_COMMIT_SHA}/''${TARGET}/garage s3://garagehq.deuxfleurs.fr/_releases/''${CI_COMMIT_TAG:-$CI_COMMIT_SHA}/''${TARGET}/garage
} }

43
src/api/admin/Cargo.toml Normal file
View file

@ -0,0 +1,43 @@
[package]
name = "garage_api_admin"
version = "1.0.1"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
description = "Admin API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md"
[lib]
path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
garage_model.workspace = true
garage_table.workspace = true
garage_util.workspace = true
garage_rpc.workspace = true
garage_api_common.workspace = true
argon2.workspace = true
async-trait.workspace = true
err-derive.workspace = true
hex.workspace = true
tracing.workspace = true
futures.workspace = true
tokio.workspace = true
http.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
url.workspace = true
serde.workspace = true
serde_json.workspace = true
opentelemetry.workspace = true
opentelemetry-prometheus = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
[features]
metrics = [ "opentelemetry-prometheus", "prometheus" ]

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use argon2::password_hash::PasswordHash; use argon2::password_hash::PasswordHash;
use async_trait::async_trait;
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW}; use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
@ -20,15 +19,15 @@ use garage_rpc::system::ClusterHealthStatus;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use garage_util::socket_address::UnixOrTCPSocketAddress; use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::generic_server::*; use garage_api_common::generic_server::*;
use garage_api_common::helpers::*;
use crate::admin::bucket::*; use crate::bucket::*;
use crate::admin::cluster::*; use crate::cluster::*;
use crate::admin::error::*; use crate::error::*;
use crate::admin::key::*; use crate::key::*;
use crate::admin::router_v0; use crate::router_v0;
use crate::admin::router_v1::{Authorization, Endpoint}; use crate::router_v1::{Authorization, Endpoint};
use crate::helpers::*;
pub type ResBody = BoxBody<Error>; pub type ResBody = BoxBody<Error>;
@ -221,7 +220,6 @@ impl AdminApiServer {
} }
} }
#[async_trait]
impl ApiHandler for AdminApiServer { impl ApiHandler for AdminApiServer {
const API_NAME: &'static str = "admin"; const API_NAME: &'static str = "admin";
const API_NAME_DISPLAY: &'static str = "Admin"; const API_NAME_DISPLAY: &'static str = "Admin";

View file

@ -17,11 +17,12 @@ use garage_model::permission::*;
use garage_model::s3::mpu_table; use garage_model::s3::mpu_table;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use crate::admin::api_server::ResBody; use garage_api_common::common_error::CommonError;
use crate::admin::error::*; use garage_api_common::helpers::*;
use crate::admin::key::ApiBucketKeyPerm;
use crate::common_error::CommonError; use crate::api_server::ResBody;
use crate::helpers::*; use crate::error::*;
use crate::key::ApiBucketKeyPerm;
pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let buckets = garage let buckets = garage

View file

@ -12,9 +12,10 @@ use garage_rpc::layout;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use crate::admin::api_server::ResBody; use garage_api_common::helpers::{json_ok_response, parse_json_body};
use crate::admin::error::*;
use crate::helpers::{json_ok_response, parse_json_body}; use crate::api_server::ResBody;
use crate::error::*;
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let layout = garage.system.cluster_layout(); let layout = garage.system.cluster_layout();

View file

@ -1,20 +1,24 @@
use std::convert::TryFrom;
use err_derive::Error; use err_derive::Error;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::{HeaderMap, StatusCode}; use hyper::{HeaderMap, StatusCode};
pub use garage_model::helper::error::Error as HelperError; pub use garage_model::helper::error::Error as HelperError;
use crate::common_error::CommonError; use garage_api_common::common_error::{commonErrorDerivative, CommonError};
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; pub use garage_api_common::common_error::{
use crate::generic_server::ApiError; CommonErrorDerivative, OkOrBadRequest, OkOrInternalError,
use crate::helpers::*; };
use garage_api_common::generic_server::ApiError;
use garage_api_common::helpers::*;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error(display = "{}", _0)] #[error(display = "{}", _0)]
/// Error from common error /// Error from common error
Common(CommonError), Common(#[error(source)] CommonError),
// Category: cannot process // Category: cannot process
/// The API access key does not exist /// The API access key does not exist
@ -29,17 +33,21 @@ pub enum Error {
KeyAlreadyExists(String), KeyAlreadyExists(String),
} }
impl<T> From<T> for Error commonErrorDerivative!(Error);
where
CommonError: From<T>, /// FIXME: helper errors are transformed into their corresponding variants
{ /// in the Error struct, but in many case a helper error should be considered
fn from(err: T) -> Self { /// an internal error.
Error::Common(CommonError::from(err)) impl From<HelperError> for Error {
fn from(err: HelperError) -> Error {
match CommonError::try_from(err) {
Ok(ce) => Self::Common(ce),
Err(HelperError::NoSuchAccessKey(k)) => Self::NoSuchAccessKey(k),
Err(_) => unreachable!(),
}
} }
} }
impl CommonErrorDerivative for Error {}
impl Error { impl Error {
fn code(&self) -> &'static str { fn code(&self) -> &'static str {
match self { match self {

View file

@ -9,9 +9,10 @@ use garage_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::*; use garage_model::key_table::*;
use crate::admin::api_server::ResBody; use garage_api_common::helpers::*;
use crate::admin::error::*;
use crate::helpers::*; use crate::api_server::ResBody;
use crate::error::*;
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let res = garage let res = garage

View file

@ -1,3 +1,6 @@
#[macro_use]
extern crate tracing;
pub mod api_server; pub mod api_server;
mod error; mod error;
mod router_v0; mod router_v0;

View file

@ -2,8 +2,9 @@ use std::borrow::Cow;
use hyper::{Method, Request}; use hyper::{Method, Request};
use crate::admin::error::*; use garage_api_common::router_macros::*;
use crate::router_macros::*;
use crate::error::*;
router_match! {@func router_match! {@func

View file

@ -2,9 +2,10 @@ use std::borrow::Cow;
use hyper::{Method, Request}; use hyper::{Method, Request};
use crate::admin::error::*; use garage_api_common::router_macros::*;
use crate::admin::router_v0;
use crate::router_macros::*; use crate::error::*;
use crate::router_v0;
pub enum Authorization { pub enum Authorization {
None, None,

44
src/api/common/Cargo.toml Normal file
View file

@ -0,0 +1,44 @@
[package]
name = "garage_api_common"
version = "1.0.1"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
description = "Common functions for the API server crates for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md"
[lib]
path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
garage_model.workspace = true
garage_table.workspace = true
garage_util.workspace = true
bytes.workspace = true
chrono.workspace = true
crypto-common.workspace = true
err-derive.workspace = true
hex.workspace = true
hmac.workspace = true
idna.workspace = true
tracing.workspace = true
nom.workspace = true
pin-project.workspace = true
sha2.workspace = true
futures.workspace = true
tokio.workspace = true
http.workspace = true
http-body-util.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
hyper-util.workspace = true
url.workspace = true
serde.workspace = true
serde_json.workspace = true
opentelemetry.workspace = true

View file

@ -1,3 +1,5 @@
use std::convert::TryFrom;
use err_derive::Error; use err_derive::Error;
use hyper::StatusCode; use hyper::StatusCode;
@ -55,6 +57,35 @@ pub enum CommonError {
InvalidBucketName(String), InvalidBucketName(String),
} }
#[macro_export]
macro_rules! commonErrorDerivative {
( $error_struct: ident ) => {
impl From<garage_util::error::Error> for $error_struct {
fn from(err: garage_util::error::Error) -> Self {
Self::Common(CommonError::InternalError(err))
}
}
impl From<http::Error> for $error_struct {
fn from(err: http::Error) -> Self {
Self::Common(CommonError::Http(err))
}
}
impl From<hyper::Error> for $error_struct {
fn from(err: hyper::Error) -> Self {
Self::Common(CommonError::Hyper(err))
}
}
impl From<hyper::header::ToStrError> for $error_struct {
fn from(err: hyper::header::ToStrError) -> Self {
Self::Common(CommonError::InvalidHeader(err))
}
}
impl CommonErrorDerivative for $error_struct {}
};
}
pub use commonErrorDerivative;
impl CommonError { impl CommonError {
pub fn http_status_code(&self) -> StatusCode { pub fn http_status_code(&self) -> StatusCode {
match self { match self {
@ -97,18 +128,39 @@ impl CommonError {
} }
} }
impl From<HelperError> for CommonError { impl TryFrom<HelperError> for CommonError {
fn from(err: HelperError) -> Self { type Error = HelperError;
fn try_from(err: HelperError) -> Result<Self, HelperError> {
match err { match err {
HelperError::Internal(i) => Self::InternalError(i), HelperError::Internal(i) => Ok(Self::InternalError(i)),
HelperError::BadRequest(b) => Self::BadRequest(b), HelperError::BadRequest(b) => Ok(Self::BadRequest(b)),
HelperError::InvalidBucketName(n) => Self::InvalidBucketName(n), HelperError::InvalidBucketName(n) => Ok(Self::InvalidBucketName(n)),
HelperError::NoSuchBucket(n) => Self::NoSuchBucket(n), HelperError::NoSuchBucket(n) => Ok(Self::NoSuchBucket(n)),
e => Self::bad_request(format!("{}", e)), e => Err(e),
} }
} }
} }
/// This function converts HelperErrors into CommonErrors,
/// for variants that exist in CommonError.
/// This is used for helper functions that might return InvalidBucketName
/// or NoSuchBucket for instance, and we want to pass that error
/// up to our caller.
pub fn pass_helper_error(err: HelperError) -> CommonError {
match CommonError::try_from(err) {
Ok(e) => e,
Err(e) => panic!("Helper error `{}` should hot have happenned here", e),
}
}
pub fn helper_error_as_internal(err: HelperError) -> CommonError {
match err {
HelperError::Internal(e) => CommonError::InternalError(e),
e => CommonError::InternalError(GarageError::Message(e.to_string())),
}
}
pub trait CommonErrorDerivative: From<CommonError> { pub trait CommonErrorDerivative: From<CommonError> {
fn internal_error<M: ToString>(msg: M) -> Self { fn internal_error<M: ToString>(msg: M) -> Self {
Self::from(CommonError::InternalError(GarageError::Message( Self::from(CommonError::InternalError(GarageError::Message(

170
src/api/common/cors.rs Normal file
View file

@ -0,0 +1,170 @@
use std::sync::Arc;
use http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
};
use hyper::{body::Body, body::Incoming as IncomingBody, Request, Response, StatusCode};
use garage_model::bucket_table::{BucketParams, CorsRule as GarageCorsRule};
use garage_model::garage::Garage;
use crate::common_error::{
helper_error_as_internal, CommonError, OkOrBadRequest, OkOrInternalError,
};
use crate::helpers::*;
pub fn find_matching_cors_rule<'a>(
bucket_params: &'a BucketParams,
req: &Request<impl Body>,
) -> Result<Option<&'a GarageCorsRule>, CommonError> {
if let Some(cors_config) = bucket_params.cors_config.get() {
if let Some(origin) = req.headers().get("Origin") {
let origin = origin.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
return Ok(cors_config.iter().find(|rule| {
cors_rule_matches(rule, origin, req.method().as_ref(), request_headers.iter())
}));
}
}
Ok(None)
}
pub fn cors_rule_matches<'a, HI, S>(
rule: &GarageCorsRule,
origin: &'a str,
method: &'a str,
mut request_headers: HI,
) -> bool
where
HI: Iterator<Item = S>,
S: AsRef<str>,
{
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
&& request_headers.all(|h| {
rule.allow_headers
.iter()
.any(|x| x == "*" || x == h.as_ref())
})
}
pub fn add_cors_headers(
resp: &mut Response<impl Body>,
rule: &GarageCorsRule,
) -> Result<(), http::header::InvalidHeaderValue> {
let h = resp.headers_mut();
h.insert(
ACCESS_CONTROL_ALLOW_ORIGIN,
rule.allow_origins.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_METHODS,
rule.allow_methods.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_HEADERS,
rule.allow_headers.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_EXPOSE_HEADERS,
rule.expose_headers.join(", ").parse()?,
);
Ok(())
}
pub async fn handle_options_api(
garage: Arc<Garage>,
req: &Request<IncomingBody>,
bucket_name: Option<String>,
) -> Result<Response<EmptyBody>, CommonError> {
// 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
.map_err(helper_error_as_internal)?;
if let Some(id) = bucket_id {
let bucket = garage
.bucket_helper()
.get_existing_bucket(id)
.await
.map_err(helper_error_as_internal)?;
let bucket_params = bucket.state.into_option().unwrap();
handle_options_for_bucket(req, &bucket_params)
} 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(EmptyBody::new())?)
}
} 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(EmptyBody::new())?)
}
}
pub fn handle_options_for_bucket(
req: &Request<IncomingBody>,
bucket_params: &BucketParams,
) -> Result<Response<EmptyBody>, CommonError> {
let origin = req
.headers()
.get("Origin")
.ok_or_bad_request("Missing Origin header")?
.to_str()?;
let request_method = req
.headers()
.get(ACCESS_CONTROL_REQUEST_METHOD)
.ok_or_bad_request("Missing Access-Control-Request-Method header")?
.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
if let Some(cors_config) = bucket_params.cors_config.get() {
let matching_rule = cors_config
.iter()
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
if let Some(rule) = matching_rule {
let mut resp = Response::builder()
.status(StatusCode::OK)
.body(EmptyBody::new())?;
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
return Ok(resp);
}
}
Err(CommonError::Forbidden(
"This CORS request is not allowed.".into(),
))
}

View file

@ -4,8 +4,6 @@ use std::os::unix::fs::PermissionsExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait;
use futures::future::Future; use futures::future::Future;
use futures::stream::{futures_unordered::FuturesUnordered, StreamExt}; use futures::stream::{futures_unordered::FuturesUnordered, StreamExt};
@ -36,7 +34,7 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::helpers::{BoxBody, ErrorBody}; use crate::helpers::{BoxBody, ErrorBody};
pub(crate) trait ApiEndpoint: Send + Sync + 'static { pub trait ApiEndpoint: Send + Sync + 'static {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn add_span_attributes(&self, span: SpanRef<'_>); fn add_span_attributes(&self, span: SpanRef<'_>);
} }
@ -47,8 +45,7 @@ pub trait ApiError: std::error::Error + Send + Sync + 'static {
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody; fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody;
} }
#[async_trait] pub trait ApiHandler: Send + Sync + 'static {
pub(crate) trait ApiHandler: Send + Sync + 'static {
const API_NAME: &'static str; const API_NAME: &'static str;
const API_NAME_DISPLAY: &'static str; const API_NAME_DISPLAY: &'static str;
@ -56,14 +53,14 @@ pub(crate) trait ApiHandler: Send + Sync + 'static {
type Error: ApiError; type Error: ApiError;
fn parse_endpoint(&self, r: &Request<IncomingBody>) -> Result<Self::Endpoint, Self::Error>; fn parse_endpoint(&self, r: &Request<IncomingBody>) -> Result<Self::Endpoint, Self::Error>;
async fn handle( fn handle(
&self, &self,
req: Request<IncomingBody>, req: Request<IncomingBody>,
endpoint: Self::Endpoint, endpoint: Self::Endpoint,
) -> Result<Response<BoxBody<Self::Error>>, Self::Error>; ) -> impl Future<Output = Result<Response<BoxBody<Self::Error>>, Self::Error>> + Send;
} }
pub(crate) struct ApiServer<A: ApiHandler> { pub struct ApiServer<A: ApiHandler> {
region: String, region: String,
api_handler: A, api_handler: A,
@ -248,13 +245,11 @@ impl<A: ApiHandler> ApiServer<A> {
// ==== helper functions ==== // ==== helper functions ====
#[async_trait]
pub trait Accept: Send + Sync + 'static { pub trait Accept: Send + Sync + 'static {
type Stream: AsyncRead + AsyncWrite + Send + Sync + 'static; type Stream: AsyncRead + AsyncWrite + Send + Sync + 'static;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)>; fn accept(&self) -> impl Future<Output = std::io::Result<(Self::Stream, String)>> + Send;
} }
#[async_trait]
impl Accept for TcpListener { impl Accept for TcpListener {
type Stream = TcpStream; type Stream = TcpStream;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> { async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {
@ -266,7 +261,6 @@ impl Accept for TcpListener {
pub struct UnixListenerOn(pub UnixListener, pub String); pub struct UnixListenerOn(pub UnixListener, pub String);
#[async_trait]
impl Accept for UnixListenerOn { impl Accept for UnixListenerOn {
type Stream = UnixStream; type Stream = UnixStream;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> { async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {

View file

@ -363,9 +363,9 @@ mod tests {
} }
#[derive(Serialize)] #[derive(Serialize)]
pub(crate) struct CustomApiErrorBody { pub struct CustomApiErrorBody {
pub(crate) code: String, pub code: String,
pub(crate) message: String, pub message: String,
pub(crate) region: String, pub region: String,
pub(crate) path: String, pub path: String,
} }

12
src/api/common/lib.rs Normal file
View file

@ -0,0 +1,12 @@
//! Crate for serving a S3 compatible API
#[macro_use]
extern crate tracing;
pub mod common_error;
pub mod cors;
pub mod encoding;
pub mod generic_server;
pub mod helpers;
pub mod router_macros;
pub mod signature;

View file

@ -1,5 +1,6 @@
/// This macro is used to generate very repetitive match {} blocks in this module /// This macro is used to generate very repetitive match {} blocks in this module
/// It is _not_ made to be used anywhere else /// It is _not_ made to be used anywhere else
#[macro_export]
macro_rules! router_match { macro_rules! router_match {
(@match $enum:expr , [ $($endpoint:ident,)* ]) => {{ (@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
// usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] } // usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
@ -133,6 +134,7 @@ macro_rules! router_match {
/// This macro is used to generate part of the code in this module. It must be called only one, and /// This macro is used to generate part of the code in this module. It must be called only one, and
/// is useless outside of this module. /// is useless outside of this module.
#[macro_export]
macro_rules! generateQueryParameters { macro_rules! generateQueryParameters {
( (
keywords: [ $($kw_param:expr => $kw_name: ident),* ], keywords: [ $($kw_param:expr => $kw_name: ident),* ],
@ -220,5 +222,5 @@ macro_rules! generateQueryParameters {
} }
} }
pub(crate) use generateQueryParameters; pub use generateQueryParameters;
pub(crate) use router_match; pub use router_match;

View file

@ -518,7 +518,7 @@ impl Authorization {
}) })
} }
pub(crate) fn parse_form(params: &HeaderMap) -> Result<Self, Error> { pub fn parse_form(params: &HeaderMap) -> Result<Self, Error> {
let algorithm = params let algorithm = params
.get(X_AMZ_ALGORITHM) .get(X_AMZ_ALGORITHM)
.ok_or_bad_request("Missing X-Amz-Algorithm header")? .ok_or_bad_request("Missing X-Amz-Algorithm header")?

37
src/api/k2v/Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "garage_api_k2v"
version = "1.0.1"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
description = "K2V API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md"
[lib]
path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
garage_model = { workspace = true, features = [ "k2v" ] }
garage_table.workspace = true
garage_util = { workspace = true, features = [ "k2v" ] }
garage_api_common.workspace = true
base64.workspace = true
err-derive.workspace = true
tracing.workspace = true
futures.workspace = true
tokio.workspace = true
http.workspace = true
http-body-util.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
percent-encoding.workspace = true
url.workspace = true
serde.workspace = true
serde_json.workspace = true
opentelemetry.workspace = true

View file

@ -1,7 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use hyper::{body::Incoming as IncomingBody, Method, Request, Response}; use hyper::{body::Incoming as IncomingBody, Method, Request, Response};
use tokio::sync::watch; use tokio::sync::watch;
@ -12,26 +10,25 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use crate::generic_server::*; use garage_api_common::cors::*;
use crate::k2v::error::*; use garage_api_common::generic_server::*;
use garage_api_common::helpers::*;
use garage_api_common::signature::verify_request;
use crate::signature::verify_request; use crate::batch::*;
use crate::error::*;
use crate::index::*;
use crate::item::*;
use crate::router::Endpoint;
use crate::helpers::*; pub use garage_api_common::signature::streaming::ReqBody;
use crate::k2v::batch::*;
use crate::k2v::index::*;
use crate::k2v::item::*;
use crate::k2v::router::Endpoint;
use crate::s3::cors::*;
pub use crate::signature::streaming::ReqBody;
pub type ResBody = BoxBody<Error>; pub type ResBody = BoxBody<Error>;
pub struct K2VApiServer { pub struct K2VApiServer {
garage: Arc<Garage>, garage: Arc<Garage>,
} }
pub(crate) struct K2VApiEndpoint { pub struct K2VApiEndpoint {
bucket_name: String, bucket_name: String,
endpoint: Endpoint, endpoint: Endpoint,
} }
@ -49,7 +46,6 @@ impl K2VApiServer {
} }
} }
#[async_trait]
impl ApiHandler for K2VApiServer { impl ApiHandler for K2VApiServer {
const API_NAME: &'static str = "k2v"; const API_NAME: &'static str = "k2v";
const API_NAME_DISPLAY: &'static str = "K2V"; const API_NAME_DISPLAY: &'static str = "K2V";
@ -90,11 +86,13 @@ impl ApiHandler for K2VApiServer {
let bucket_id = garage let bucket_id = garage
.bucket_helper() .bucket_helper()
.resolve_bucket(&bucket_name, &api_key) .resolve_bucket(&bucket_name, &api_key)
.await?; .await
.map_err(pass_helper_error)?;
let bucket = garage let bucket = garage
.bucket_helper() .bucket_helper()
.get_existing_bucket(bucket_id) .get_existing_bucket(bucket_id)
.await?; .await
.map_err(helper_error_as_internal)?;
let bucket_params = bucket.state.into_option().unwrap(); let bucket_params = bucket.state.into_option().unwrap();
let allowed = match endpoint.authorization_type() { let allowed = match endpoint.authorization_type() {

View file

@ -4,13 +4,14 @@ use serde::{Deserialize, Serialize};
use garage_table::{EnumerationOrder, TableSchema}; use garage_table::{EnumerationOrder, TableSchema};
use garage_model::k2v::causality::*;
use garage_model::k2v::item_table::*; use garage_model::k2v::item_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::k2v::api_server::{ReqBody, ResBody};
use crate::k2v::error::*; use crate::api_server::{ReqBody, ResBody};
use crate::k2v::range::read_range; use crate::error::*;
use crate::item::parse_causality_token;
use crate::range::read_range;
pub async fn handle_insert_batch( pub async fn handle_insert_batch(
ctx: ReqCtx, ctx: ReqCtx,
@ -23,7 +24,7 @@ pub async fn handle_insert_batch(
let mut items2 = vec![]; let mut items2 = vec![];
for it in items { for it in items {
let ct = it.ct.map(|s| CausalContext::parse_helper(&s)).transpose()?; let ct = it.ct.map(|s| parse_causality_token(&s)).transpose()?;
let v = match it.v { let v = match it.v {
Some(vs) => DvvsValue::Value( Some(vs) => DvvsValue::Value(
BASE64_STANDARD BASE64_STANDARD
@ -281,7 +282,8 @@ pub(crate) async fn handle_poll_range(
query.seen_marker, query.seen_marker,
timeout_msec, timeout_msec,
) )
.await?; .await
.map_err(pass_helper_error)?;
if let Some((items, seen_marker)) = resp { if let Some((items, seen_marker)) = resp {
let resp = PollRangeResponse { let resp = PollRangeResponse {

View file

@ -2,18 +2,21 @@ use err_derive::Error;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::{HeaderMap, StatusCode}; use hyper::{HeaderMap, StatusCode};
use crate::common_error::CommonError; use garage_api_common::common_error::{commonErrorDerivative, CommonError};
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; pub(crate) use garage_api_common::common_error::{helper_error_as_internal, pass_helper_error};
use crate::generic_server::ApiError; pub use garage_api_common::common_error::{
use crate::helpers::*; CommonErrorDerivative, OkOrBadRequest, OkOrInternalError,
use crate::signature::error::Error as SignatureError; };
use garage_api_common::generic_server::ApiError;
use garage_api_common::helpers::*;
use garage_api_common::signature::error::Error as SignatureError;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error(display = "{}", _0)] #[error(display = "{}", _0)]
/// Error from common error /// Error from common error
Common(CommonError), Common(#[error(source)] CommonError),
// Category: cannot process // Category: cannot process
/// Authorization Header Malformed /// Authorization Header Malformed
@ -28,6 +31,10 @@ pub enum Error {
#[error(display = "Invalid base64: {}", _0)] #[error(display = "Invalid base64: {}", _0)]
InvalidBase64(#[error(source)] base64::DecodeError), InvalidBase64(#[error(source)] base64::DecodeError),
/// Invalid causality token
#[error(display = "Invalid causality token")]
InvalidCausalityToken,
/// The client asked for an invalid return format (invalid Accept header) /// The client asked for an invalid return format (invalid Accept header)
#[error(display = "Not acceptable: {}", _0)] #[error(display = "Not acceptable: {}", _0)]
NotAcceptable(String), NotAcceptable(String),
@ -37,16 +44,7 @@ pub enum Error {
InvalidUtf8Str(#[error(source)] std::str::Utf8Error), InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
} }
impl<T> From<T> for Error commonErrorDerivative!(Error);
where
CommonError: From<T>,
{
fn from(err: T) -> Self {
Error::Common(CommonError::from(err))
}
}
impl CommonErrorDerivative for Error {}
impl From<SignatureError> for Error { impl From<SignatureError> for Error {
fn from(err: SignatureError) -> Self { fn from(err: SignatureError) -> Self {
@ -72,6 +70,7 @@ impl Error {
Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed",
Error::InvalidBase64(_) => "InvalidBase64", Error::InvalidBase64(_) => "InvalidBase64",
Error::InvalidUtf8Str(_) => "InvalidUtf8String", Error::InvalidUtf8Str(_) => "InvalidUtf8String",
Error::InvalidCausalityToken => "CausalityToken",
} }
} }
} }
@ -85,7 +84,8 @@ impl ApiError for Error {
Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE, Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
Error::AuthorizationHeaderMalformed(_) Error::AuthorizationHeaderMalformed(_)
| Error::InvalidBase64(_) | Error::InvalidBase64(_)
| Error::InvalidUtf8Str(_) => StatusCode::BAD_REQUEST, | Error::InvalidUtf8Str(_)
| Error::InvalidCausalityToken => StatusCode::BAD_REQUEST,
} }
} }

View file

@ -5,10 +5,11 @@ use garage_table::util::*;
use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES};
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::k2v::api_server::ResBody;
use crate::k2v::error::*; use crate::api_server::ResBody;
use crate::k2v::range::read_range; use crate::error::*;
use crate::range::read_range;
pub async fn handle_read_index( pub async fn handle_read_index(
ctx: ReqCtx, ctx: ReqCtx,

View file

@ -6,9 +6,10 @@ use hyper::{Request, Response, StatusCode};
use garage_model::k2v::causality::*; use garage_model::k2v::causality::*;
use garage_model::k2v::item_table::*; use garage_model::k2v::item_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::k2v::api_server::{ReqBody, ResBody};
use crate::k2v::error::*; use crate::api_server::{ReqBody, ResBody};
use crate::error::*;
pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token"; pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token";
@ -18,6 +19,10 @@ pub enum ReturnFormat {
Either, Either,
} }
pub(crate) fn parse_causality_token(s: &str) -> Result<CausalContext, Error> {
CausalContext::parse(s).ok_or(Error::InvalidCausalityToken)
}
impl ReturnFormat { impl ReturnFormat {
pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> { pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> {
let accept = match req.headers().get(header::ACCEPT) { let accept = match req.headers().get(header::ACCEPT) {
@ -136,7 +141,7 @@ pub async fn handle_insert_item(
.get(X_GARAGE_CAUSALITY_TOKEN) .get(X_GARAGE_CAUSALITY_TOKEN)
.map(|s| s.to_str()) .map(|s| s.to_str())
.transpose()? .transpose()?
.map(CausalContext::parse_helper) .map(parse_causality_token)
.transpose()?; .transpose()?;
let body = http_body_util::BodyExt::collect(req.into_body()) let body = http_body_util::BodyExt::collect(req.into_body())
@ -176,7 +181,7 @@ pub async fn handle_delete_item(
.get(X_GARAGE_CAUSALITY_TOKEN) .get(X_GARAGE_CAUSALITY_TOKEN)
.map(|s| s.to_str()) .map(|s| s.to_str())
.transpose()? .transpose()?
.map(CausalContext::parse_helper) .map(parse_causality_token)
.transpose()?; .transpose()?;
let value = DvvsValue::Deleted; let value = DvvsValue::Deleted;

View file

@ -1,3 +1,6 @@
#[macro_use]
extern crate tracing;
pub mod api_server; pub mod api_server;
mod error; mod error;
mod router; mod router;

View file

@ -7,8 +7,9 @@ use std::sync::Arc;
use garage_table::replication::TableShardedReplication; use garage_table::replication::TableShardedReplication;
use garage_table::*; use garage_table::*;
use crate::helpers::key_after_prefix; use garage_api_common::helpers::key_after_prefix;
use crate::k2v::error::*;
use crate::error::*;
/// Read range in a Garage table. /// Read range in a Garage table.
/// Returns (entries, more?, nextStart) /// Returns (entries, more?, nextStart)

View file

@ -1,11 +1,11 @@
use crate::k2v::error::*; use crate::error::*;
use std::borrow::Cow; use std::borrow::Cow;
use hyper::{Method, Request}; use hyper::{Method, Request};
use crate::helpers::Authorization; use garage_api_common::helpers::Authorization;
use crate::router_macros::{generateQueryParameters, router_match}; use garage_api_common::router_macros::{generateQueryParameters, router_match};
router_match! {@func router_match! {@func

View file

@ -1,17 +0,0 @@
//! Crate for serving a S3 compatible API
#[macro_use]
extern crate tracing;
pub mod common_error;
mod encoding;
pub mod generic_server;
pub mod helpers;
mod router_macros;
/// This mode is public only to help testing. Don't expect stability here
pub mod signature;
pub mod admin;
#[cfg(feature = "k2v")]
pub mod k2v;
pub mod s3;

View file

@ -1,5 +1,5 @@
[package] [package]
name = "garage_api" name = "garage_api_s3"
version = "1.0.1" version = "1.0.1"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
@ -20,30 +20,24 @@ garage_block.workspace = true
garage_net.workspace = true garage_net.workspace = true
garage_util.workspace = true garage_util.workspace = true
garage_rpc.workspace = true garage_rpc.workspace = true
garage_api_common.workspace = true
aes-gcm.workspace = true aes-gcm.workspace = true
argon2.workspace = true
async-compression.workspace = true async-compression.workspace = true
async-trait.workspace = true
base64.workspace = true base64.workspace = true
bytes.workspace = true bytes.workspace = true
chrono.workspace = true chrono.workspace = true
crc32fast.workspace = true crc32fast.workspace = true
crc32c.workspace = true crc32c.workspace = true
crypto-common.workspace = true
err-derive.workspace = true err-derive.workspace = true
hex.workspace = true hex.workspace = true
hmac.workspace = true
idna.workspace = true
tracing.workspace = true tracing.workspace = true
md-5.workspace = true md-5.workspace = true
nom.workspace = true
pin-project.workspace = true pin-project.workspace = true
sha1.workspace = true sha1.workspace = true
sha2.workspace = true sha2.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true
tokio.workspace = true tokio.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
tokio-util.workspace = true tokio-util.workspace = true
@ -54,21 +48,13 @@ httpdate.workspace = true
http-range.workspace = true http-range.workspace = true
http-body-util.workspace = true http-body-util.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] } hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
hyper-util.workspace = true
multer.workspace = true multer.workspace = true
percent-encoding.workspace = true percent-encoding.workspace = true
roxmltree.workspace = true roxmltree.workspace = true
url.workspace = true url.workspace = true
serde.workspace = true serde.workspace = true
serde_bytes.workspace = true
serde_json.workspace = true serde_json.workspace = true
quick-xml.workspace = true quick-xml.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
opentelemetry-prometheus = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
[features]
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
metrics = [ "opentelemetry-prometheus", "prometheus" ]

View file

@ -1,7 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use hyper::header; use hyper::header;
use hyper::{body::Incoming as IncomingBody, Request, Response}; use hyper::{body::Incoming as IncomingBody, Request, Response};
use tokio::sync::watch; use tokio::sync::watch;
@ -14,33 +12,33 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::Key; use garage_model::key_table::Key;
use crate::generic_server::*; use garage_api_common::cors::*;
use crate::s3::error::*; use garage_api_common::generic_server::*;
use garage_api_common::helpers::*;
use garage_api_common::signature::verify_request;
use crate::signature::verify_request; use crate::bucket::*;
use crate::copy::*;
use crate::cors::*;
use crate::delete::*;
use crate::error::*;
use crate::get::*;
use crate::lifecycle::*;
use crate::list::*;
use crate::multipart::*;
use crate::post_object::handle_post_object;
use crate::put::*;
use crate::router::Endpoint;
use crate::website::*;
use crate::helpers::*; pub use garage_api_common::signature::streaming::ReqBody;
use crate::s3::bucket::*;
use crate::s3::copy::*;
use crate::s3::cors::*;
use crate::s3::delete::*;
use crate::s3::get::*;
use crate::s3::lifecycle::*;
use crate::s3::list::*;
use crate::s3::multipart::*;
use crate::s3::post_object::handle_post_object;
use crate::s3::put::*;
use crate::s3::router::Endpoint;
use crate::s3::website::*;
pub use crate::signature::streaming::ReqBody;
pub type ResBody = BoxBody<Error>; pub type ResBody = BoxBody<Error>;
pub struct S3ApiServer { pub struct S3ApiServer {
garage: Arc<Garage>, garage: Arc<Garage>,
} }
pub(crate) struct S3ApiEndpoint { pub struct S3ApiEndpoint {
bucket_name: Option<String>, bucket_name: Option<String>,
endpoint: Endpoint, endpoint: Endpoint,
} }
@ -70,7 +68,6 @@ impl S3ApiServer {
} }
} }
#[async_trait]
impl ApiHandler for S3ApiServer { impl ApiHandler for S3ApiServer {
const API_NAME: &'static str = "s3"; const API_NAME: &'static str = "s3";
const API_NAME_DISPLAY: &'static str = "S3"; const API_NAME_DISPLAY: &'static str = "S3";
@ -150,7 +147,8 @@ impl ApiHandler for S3ApiServer {
let bucket_id = garage let bucket_id = garage
.bucket_helper() .bucket_helper()
.resolve_bucket(&bucket_name, &api_key) .resolve_bucket(&bucket_name, &api_key)
.await?; .await
.map_err(pass_helper_error)?;
let bucket = garage let bucket = garage
.bucket_helper() .bucket_helper()
.get_existing_bucket(bucket_id) .get_existing_bucket(bucket_id)

View file

@ -13,12 +13,13 @@ use garage_util::crdt::*;
use garage_util::data::*; use garage_util::data::*;
use garage_util::time::*; use garage_util::time::*;
use crate::common_error::CommonError; use garage_api_common::common_error::CommonError;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody}; use garage_api_common::signature::verify_signed_content;
use crate::s3::error::*;
use crate::s3::xml as s3_xml; use crate::api_server::{ReqBody, ResBody};
use crate::signature::verify_signed_content; use crate::error::*;
use crate::xml as s3_xml;
pub fn handle_get_bucket_location(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub fn handle_get_bucket_location(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { garage, .. } = ctx; let ReqCtx { garage, .. } = ctx;

View file

@ -15,7 +15,7 @@ use garage_util::error::OkOrMessage;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use crate::s3::error::*; use crate::error::*;
pub const X_AMZ_CHECKSUM_ALGORITHM: HeaderName = pub const X_AMZ_CHECKSUM_ALGORITHM: HeaderName =
HeaderName::from_static("x-amz-checksum-algorithm"); HeaderName::from_static("x-amz-checksum-algorithm");

View file

@ -20,15 +20,16 @@ use garage_model::s3::mpu_table::*;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody};
use crate::s3::checksum::*; use crate::api_server::{ReqBody, ResBody};
use crate::s3::encryption::EncryptionParams; use crate::checksum::*;
use crate::s3::error::*; use crate::encryption::EncryptionParams;
use crate::s3::get::full_object_byte_stream; use crate::error::*;
use crate::s3::multipart; use crate::get::full_object_byte_stream;
use crate::s3::put::{get_headers, save_stream, ChecksumMode, SaveStreamResult}; use crate::multipart;
use crate::s3::xml::{self as s3_xml, xmlns_tag}; use crate::put::{get_headers, save_stream, ChecksumMode, SaveStreamResult};
use crate::xml::{self as s3_xml, xmlns_tag};
// -------- CopyObject --------- // -------- CopyObject ---------
@ -655,7 +656,8 @@ async fn get_copy_source(ctx: &ReqCtx, req: &Request<ReqBody>) -> Result<Object,
let source_bucket_id = garage let source_bucket_id = garage
.bucket_helper() .bucket_helper()
.resolve_bucket(&source_bucket.to_string(), api_key) .resolve_bucket(&source_bucket.to_string(), api_key)
.await?; .await
.map_err(pass_helper_error)?;
if !api_key.allow_read(&source_bucket_id) { if !api_key.allow_read(&source_bucket_id) {
return Err(Error::forbidden(format!( return Err(Error::forbidden(format!(
@ -861,7 +863,7 @@ pub struct CopyPartResult {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::s3::xml::to_xml_with_header; use crate::xml::to_xml_with_header;
#[test] #[test]
fn copy_object_result() -> Result<(), Error> { fn copy_object_result() -> Result<(), Error> {

View file

@ -1,30 +1,21 @@
use quick_xml::de::from_reader; use quick_xml::de::from_reader;
use std::sync::Arc;
use http::header::{ use hyper::{header::HeaderName, Method, Request, Response, StatusCode};
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
};
use hyper::{
body::Body, body::Incoming as IncomingBody, header::HeaderName, Method, Request, Response,
StatusCode,
};
use http_body_util::BodyExt; use http_body_util::BodyExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::common_error::CommonError; use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule};
use crate::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody};
use crate::s3::error::*;
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
use crate::signature::verify_signed_content;
use garage_model::bucket_table::{Bucket, BucketParams, CorsRule as GarageCorsRule};
use garage_model::garage::Garage;
use garage_util::data::*; use garage_util::data::*;
use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody};
use crate::error::*;
use crate::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
pub async fn handle_get_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; let ReqCtx { bucket_params, .. } = ctx;
if let Some(cors) = bucket_params.cors_config.get() { if let Some(cors) = bucket_params.cors_config.get() {
@ -99,154 +90,6 @@ pub async fn handle_put_cors(
.body(empty_body())?) .body(empty_body())?)
} }
pub async fn handle_options_api(
garage: Arc<Garage>,
req: &Request<IncomingBody>,
bucket_name: Option<String>,
) -> Result<Response<EmptyBody>, CommonError> {
// 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_helper().get_existing_bucket(id).await?;
let bucket_params = bucket.state.into_option().unwrap();
handle_options_for_bucket(req, &bucket_params)
} 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(EmptyBody::new())?)
}
} 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(EmptyBody::new())?)
}
}
pub fn handle_options_for_bucket(
req: &Request<IncomingBody>,
bucket_params: &BucketParams,
) -> Result<Response<EmptyBody>, CommonError> {
let origin = req
.headers()
.get("Origin")
.ok_or_bad_request("Missing Origin header")?
.to_str()?;
let request_method = req
.headers()
.get(ACCESS_CONTROL_REQUEST_METHOD)
.ok_or_bad_request("Missing Access-Control-Request-Method header")?
.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
if let Some(cors_config) = bucket_params.cors_config.get() {
let matching_rule = cors_config
.iter()
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
if let Some(rule) = matching_rule {
let mut resp = Response::builder()
.status(StatusCode::OK)
.body(EmptyBody::new())?;
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
return Ok(resp);
}
}
Err(CommonError::Forbidden(
"This CORS request is not allowed.".into(),
))
}
pub fn find_matching_cors_rule<'a>(
bucket_params: &'a BucketParams,
req: &Request<impl Body>,
) -> Result<Option<&'a GarageCorsRule>, Error> {
if let Some(cors_config) = bucket_params.cors_config.get() {
if let Some(origin) = req.headers().get("Origin") {
let origin = origin.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
return Ok(cors_config.iter().find(|rule| {
cors_rule_matches(rule, origin, req.method().as_ref(), request_headers.iter())
}));
}
}
Ok(None)
}
fn cors_rule_matches<'a, HI, S>(
rule: &GarageCorsRule,
origin: &'a str,
method: &'a str,
mut request_headers: HI,
) -> bool
where
HI: Iterator<Item = S>,
S: AsRef<str>,
{
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
&& request_headers.all(|h| {
rule.allow_headers
.iter()
.any(|x| x == "*" || x == h.as_ref())
})
}
pub fn add_cors_headers(
resp: &mut Response<impl Body>,
rule: &GarageCorsRule,
) -> Result<(), http::header::InvalidHeaderValue> {
let h = resp.headers_mut();
h.insert(
ACCESS_CONTROL_ALLOW_ORIGIN,
rule.allow_origins.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_METHODS,
rule.allow_methods.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_HEADERS,
rule.allow_headers.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_EXPOSE_HEADERS,
rule.expose_headers.join(", ").parse()?,
);
Ok(())
}
// ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ---- // ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ----
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]

View file

@ -5,12 +5,13 @@ use garage_util::data::*;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody}; use garage_api_common::signature::verify_signed_content;
use crate::s3::error::*;
use crate::s3::put::next_timestamp; use crate::api_server::{ReqBody, ResBody};
use crate::s3::xml as s3_xml; use crate::error::*;
use crate::signature::verify_signed_content; use crate::put::next_timestamp;
use crate::xml as s3_xml;
async fn handle_delete_internal(ctx: &ReqCtx, key: &str) -> Result<(Uuid, Uuid), Error> { async fn handle_delete_internal(ctx: &ReqCtx, key: &str) -> Result<(Uuid, Uuid), Error> {
let ReqCtx { let ReqCtx {

View file

@ -28,9 +28,10 @@ use garage_util::migrate::Migrate;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionMetaInner}; use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionMetaInner};
use crate::common_error::*; use garage_api_common::common_error::*;
use crate::s3::checksum::Md5Checksum;
use crate::s3::error::Error; use crate::checksum::Md5Checksum;
use crate::error::Error;
const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName = const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName =
HeaderName::from_static("x-amz-server-side-encryption-customer-algorithm"); HeaderName::from_static("x-amz-server-side-encryption-customer-algorithm");

View file

@ -4,19 +4,30 @@ use err_derive::Error;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::{HeaderMap, StatusCode}; use hyper::{HeaderMap, StatusCode};
use crate::common_error::CommonError; use garage_model::helper::error::Error as HelperError;
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
use crate::generic_server::ApiError; pub(crate) use garage_api_common::common_error::pass_helper_error;
use crate::helpers::*;
use crate::s3::xml as s3_xml; use garage_api_common::common_error::{
use crate::signature::error::Error as SignatureError; commonErrorDerivative, helper_error_as_internal, CommonError,
};
pub use garage_api_common::common_error::{
CommonErrorDerivative, OkOrBadRequest, OkOrInternalError,
};
use garage_api_common::generic_server::ApiError;
use garage_api_common::helpers::*;
use garage_api_common::signature::error::Error as SignatureError;
use crate::xml as s3_xml;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error(display = "{}", _0)] #[error(display = "{}", _0)]
/// Error from common error /// Error from common error
Common(CommonError), Common(#[error(source)] CommonError),
// Category: cannot process // Category: cannot process
/// Authorization Header Malformed /// Authorization Header Malformed
@ -78,17 +89,16 @@ pub enum Error {
NotImplemented(String), NotImplemented(String),
} }
impl<T> From<T> for Error commonErrorDerivative!(Error);
where
CommonError: From<T>, // Helper errors are always passed as internal errors by default.
{ // To pass the specific error code back to the client, use `pass_helper_error`.
fn from(err: T) -> Self { impl From<HelperError> for Error {
Error::Common(CommonError::from(err)) fn from(err: HelperError) -> Error {
Error::Common(helper_error_as_internal(err))
} }
} }
impl CommonErrorDerivative for Error {}
impl From<roxmltree::Error> for Error { impl From<roxmltree::Error> for Error {
fn from(err: roxmltree::Error) -> Self { fn from(err: roxmltree::Error) -> Self {
Self::InvalidXml(format!("{}", err)) Self::InvalidXml(format!("{}", err))

View file

@ -25,11 +25,12 @@ use garage_model::garage::Garage;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::ResBody;
use crate::s3::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE}; use crate::api_server::ResBody;
use crate::s3::encryption::EncryptionParams; use crate::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE};
use crate::s3::error::*; use crate::encryption::EncryptionParams;
use crate::error::*;
const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count"; const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count";
@ -68,14 +69,11 @@ fn object_headers(
// See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html // See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
let mut headers_by_name = BTreeMap::new(); let mut headers_by_name = BTreeMap::new();
for (name, value) in meta_inner.headers.iter() { for (name, value) in meta_inner.headers.iter() {
match headers_by_name.get_mut(name) { let name_lower = name.to_ascii_lowercase();
None => { headers_by_name
headers_by_name.insert(name, vec![value.as_str()]); .entry(name_lower)
} .or_insert(vec![])
Some(headers) => { .push(value.as_str());
headers.push(value.as_str());
}
}
} }
for (name, values) in headers_by_name { for (name, values) in headers_by_name {

View file

@ -1,3 +1,6 @@
#[macro_use]
extern crate tracing;
pub mod api_server; pub mod api_server;
pub mod error; pub mod error;

View file

@ -5,11 +5,12 @@ use hyper::{Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody}; use garage_api_common::signature::verify_signed_content;
use crate::s3::error::*;
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::api_server::{ReqBody, ResBody};
use crate::signature::verify_signed_content; use crate::error::*;
use crate::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
use garage_model::bucket_table::{ use garage_model::bucket_table::{
parse_lifecycle_date, Bucket, LifecycleExpiration as GarageLifecycleExpiration, parse_lifecycle_date, Bucket, LifecycleExpiration as GarageLifecycleExpiration,

View file

@ -13,13 +13,14 @@ use garage_model::s3::object_table::*;
use garage_table::EnumerationOrder; use garage_table::EnumerationOrder;
use crate::encoding::*; use garage_api_common::encoding::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody};
use crate::s3::encryption::EncryptionParams; use crate::api_server::{ReqBody, ResBody};
use crate::s3::error::*; use crate::encryption::EncryptionParams;
use crate::s3::multipart as s3_multipart; use crate::error::*;
use crate::s3::xml as s3_xml; use crate::multipart as s3_multipart;
use crate::xml as s3_xml;
const DUMMY_NAME: &str = "Dummy Key"; const DUMMY_NAME: &str = "Dummy Key";
const DUMMY_KEY: &str = "GKDummyKey"; const DUMMY_KEY: &str = "GKDummyKey";

View file

@ -15,14 +15,15 @@ use garage_model::s3::mpu_table::*;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody}; use garage_api_common::signature::verify_signed_content;
use crate::s3::checksum::*;
use crate::s3::encryption::EncryptionParams; use crate::api_server::{ReqBody, ResBody};
use crate::s3::error::*; use crate::checksum::*;
use crate::s3::put::*; use crate::encryption::EncryptionParams;
use crate::s3::xml as s3_xml; use crate::error::*;
use crate::signature::verify_signed_content; use crate::put::*;
use crate::xml as s3_xml;
// ---- // ----

View file

@ -16,15 +16,16 @@ use serde::Deserialize;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use crate::helpers::*; use garage_api_common::cors::*;
use crate::s3::api_server::ResBody; use garage_api_common::helpers::*;
use crate::s3::checksum::*; use garage_api_common::signature::payload::{verify_v4, Authorization};
use crate::s3::cors::*;
use crate::s3::encryption::EncryptionParams; use crate::api_server::ResBody;
use crate::s3::error::*; use crate::checksum::*;
use crate::s3::put::{get_headers, save_stream, ChecksumMode}; use crate::encryption::EncryptionParams;
use crate::s3::xml as s3_xml; use crate::error::*;
use crate::signature::payload::{verify_v4, Authorization}; use crate::put::{get_headers, save_stream, ChecksumMode};
use crate::xml as s3_xml;
pub async fn handle_post_object( pub async fn handle_post_object(
garage: Arc<Garage>, garage: Arc<Garage>,
@ -107,7 +108,8 @@ pub async fn handle_post_object(
let bucket_id = garage let bucket_id = garage
.bucket_helper() .bucket_helper()
.resolve_bucket(&bucket_name, &api_key) .resolve_bucket(&bucket_name, &api_key)
.await?; .await
.map_err(pass_helper_error)?;
if !api_key.allow_write(&bucket_id) { if !api_key.allow_write(&bucket_id) {
return Err(Error::forbidden("Operation is not allowed for this key.")); return Err(Error::forbidden("Operation is not allowed for this key."));

View file

@ -30,11 +30,12 @@ use garage_model::s3::block_ref_table::*;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use crate::helpers::*; use garage_api_common::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody};
use crate::s3::checksum::*; use crate::api_server::{ReqBody, ResBody};
use crate::s3::encryption::EncryptionParams; use crate::checksum::*;
use crate::s3::error::*; use crate::encryption::EncryptionParams;
use crate::error::*;
const PUT_BLOCKS_MAX_PARALLEL: usize = 3; const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
@ -622,7 +623,7 @@ pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<HeaderList
for (name, value) in headers.iter() { for (name, value) in headers.iter() {
if name.as_str().starts_with("x-amz-meta-") { if name.as_str().starts_with("x-amz-meta-") {
ret.push(( ret.push((
name.to_string(), name.as_str().to_ascii_lowercase(),
std::str::from_utf8(value.as_bytes())?.to_string(), std::str::from_utf8(value.as_bytes())?.to_string(),
)); ));
} }

View file

@ -3,9 +3,10 @@ use std::borrow::Cow;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::{HeaderMap, Method, Request}; use hyper::{HeaderMap, Method, Request};
use crate::helpers::Authorization; use garage_api_common::helpers::Authorization;
use crate::router_macros::{generateQueryParameters, router_match}; use garage_api_common::router_macros::{generateQueryParameters, router_match};
use crate::s3::error::*;
use crate::error::*;
router_match! {@func router_match! {@func

View file

@ -4,15 +4,16 @@ use http_body_util::BodyExt;
use hyper::{Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::helpers::*;
use crate::s3::api_server::{ReqBody, ResBody};
use crate::s3::error::*;
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
use crate::signature::verify_signed_content;
use garage_model::bucket_table::*; use garage_model::bucket_table::*;
use garage_util::data::*; use garage_util::data::*;
use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody};
use crate::error::*;
use crate::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; let ReqCtx { bucket_params, .. } = ctx;
if let Some(website) = bucket_params.website_config.get() { if let Some(website) = bucket_params.website_config.get() {

View file

@ -1,7 +1,7 @@
use quick_xml::se::to_string; use quick_xml::se::to_string;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use crate::s3::error::Error as ApiError; use crate::error::Error as ApiError;
pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, ApiError> { pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, ApiError> {
let mut xml = r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string(); let mut xml = r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string();

View file

@ -34,10 +34,8 @@ async-compression.workspace = true
zstd.workspace = true zstd.workspace = true
serde.workspace = true serde.workspace = true
serde_bytes.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true
tokio.workspace = true tokio.workspace = true
tokio-util.workspace = true tokio-util.workspace = true

View file

@ -4,7 +4,6 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use arc_swap::{ArcSwap, ArcSwapOption}; use arc_swap::{ArcSwap, ArcSwapOption};
use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use rand::prelude::*; use rand::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -688,7 +687,6 @@ impl BlockManager {
} }
} }
#[async_trait]
impl StreamingEndpointHandler<BlockRpc> for BlockManager { impl StreamingEndpointHandler<BlockRpc> for BlockManager {
async fn handle(self: &Arc<Self>, mut message: Req<BlockRpc>, _from: NodeID) -> Resp<BlockRpc> { async fn handle(self: &Arc<Self>, mut message: Req<BlockRpc>, _from: NodeID) -> Resp<BlockRpc> {
match message.msg() { match message.msg() {

View file

@ -13,7 +13,6 @@ path = "lib.rs"
[dependencies] [dependencies]
err-derive.workspace = true err-derive.workspace = true
hexdump.workspace = true
tracing.workspace = true tracing.workspace = true
heed = { workspace = true, optional = true } heed = { workspace = true, optional = true }

View file

@ -23,7 +23,9 @@ path = "tests/lib.rs"
[dependencies] [dependencies]
format_table.workspace = true format_table.workspace = true
garage_db.workspace = true garage_db.workspace = true
garage_api.workspace = true garage_api_admin.workspace = true
garage_api_s3.workspace = true
garage_api_k2v = { workspace = true, optional = true }
garage_block.workspace = true garage_block.workspace = true
garage_model.workspace = true garage_model.workspace = true
garage_net.workspace = true garage_net.workspace = true
@ -40,7 +42,6 @@ parse_duration.workspace = true
hex.workspace = true hex.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
rand.workspace = true
async-trait.workspace = true async-trait.workspace = true
sha1.workspace = true sha1.workspace = true
sodiumoxide.workspace = true sodiumoxide.workspace = true
@ -48,21 +49,18 @@ structopt.workspace = true
git-version.workspace = true git-version.workspace = true
serde.workspace = true serde.workspace = true
serde_bytes.workspace = true
toml.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true
tokio.workspace = true tokio.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
opentelemetry-prometheus = { workspace = true, optional = true } opentelemetry-prometheus = { workspace = true, optional = true }
opentelemetry-otlp = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
syslog-tracing = { workspace = true, optional = true } syslog-tracing = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
aws-config.workspace = true garage_api_common.workspace = true
aws-sdk-s3.workspace = true aws-sdk-s3.workspace = true
chrono.workspace = true chrono.workspace = true
http.workspace = true http.workspace = true
@ -84,7 +82,7 @@ k2v-client.workspace = true
[features] [features]
default = [ "bundled-libs", "metrics", "lmdb", "sqlite", "k2v" ] default = [ "bundled-libs", "metrics", "lmdb", "sqlite", "k2v" ]
k2v = [ "garage_util/k2v", "garage_api/k2v" ] k2v = [ "garage_util/k2v", "garage_api_k2v" ]
# Database engines # Database engines
lmdb = [ "garage_model/lmdb" ] lmdb = [ "garage_model/lmdb" ]
@ -95,7 +93,7 @@ consul-discovery = [ "garage_rpc/consul-discovery" ]
# Automatic registration and discovery via Kubernetes API # Automatic registration and discovery via Kubernetes API
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
# Prometheus exporter (/metrics endpoint). # Prometheus exporter (/metrics endpoint).
metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ] metrics = [ "garage_api_admin/metrics", "opentelemetry-prometheus" ]
# Exporter for the OpenTelemetry Collector. # Exporter for the OpenTelemetry Collector.
telemetry-otlp = [ "opentelemetry-otlp" ] telemetry-otlp = [ "opentelemetry-otlp" ]
# Logging to syslog # Logging to syslog

View file

@ -4,9 +4,11 @@ mod key;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use futures::future::FutureExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use format_table::format_table_to_string; use format_table::format_table_to_string;
@ -505,22 +507,25 @@ impl AdminRpcHandler {
} }
} }
#[async_trait]
impl EndpointHandler<AdminRpc> for AdminRpcHandler { impl EndpointHandler<AdminRpc> for AdminRpcHandler {
async fn handle( fn handle(
self: &Arc<Self>, self: &Arc<Self>,
message: &AdminRpc, message: &AdminRpc,
_from: NodeID, _from: NodeID,
) -> Result<AdminRpc, Error> { ) -> impl Future<Output = Result<AdminRpc, Error>> + Send {
let self2 = self.clone();
async move {
match message { match message {
AdminRpc::BucketOperation(bo) => self.handle_bucket_cmd(bo).await, AdminRpc::BucketOperation(bo) => self2.handle_bucket_cmd(bo).await,
AdminRpc::KeyOperation(ko) => self.handle_key_cmd(ko).await, AdminRpc::KeyOperation(ko) => self2.handle_key_cmd(ko).await,
AdminRpc::LaunchRepair(opt) => self.handle_launch_repair(opt.clone()).await, AdminRpc::LaunchRepair(opt) => self2.handle_launch_repair(opt.clone()).await,
AdminRpc::Stats(opt) => self.handle_stats(opt.clone()).await, AdminRpc::Stats(opt) => self2.handle_stats(opt.clone()).await,
AdminRpc::Worker(wo) => self.handle_worker_cmd(wo).await, AdminRpc::Worker(wo) => self2.handle_worker_cmd(wo).await,
AdminRpc::BlockOperation(bo) => self.handle_block_cmd(bo).await, AdminRpc::BlockOperation(bo) => self2.handle_block_cmd(bo).await,
AdminRpc::MetaOperation(mo) => self.handle_meta_cmd(mo).await, AdminRpc::MetaOperation(mo) => self2.handle_meta_cmd(mo).await,
m => Err(GarageError::unexpected_rpc_message(m).into()), m => Err(GarageError::unexpected_rpc_message(m).into()),
} }
} }
.boxed()
}
} }

View file

@ -1,3 +1,4 @@
use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -93,17 +94,16 @@ pub async fn launch_online_repair(
// ---- // ----
#[async_trait]
trait TableRepair: Send + Sync + 'static { trait TableRepair: Send + Sync + 'static {
type T: TableSchema; type T: TableSchema;
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication>; fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication>;
async fn process( fn process(
&mut self, &mut self,
garage: &Garage, garage: &Garage,
entry: <<Self as TableRepair>::T as TableSchema>::E, entry: <<Self as TableRepair>::T as TableSchema>::E,
) -> Result<bool, Error>; ) -> impl Future<Output = Result<bool, Error>> + Send;
} }
struct TableRepairWorker<T: TableRepair> { struct TableRepairWorker<T: TableRepair> {
@ -174,7 +174,6 @@ impl<R: TableRepair> Worker for TableRepairWorker<R> {
struct RepairVersions; struct RepairVersions;
#[async_trait]
impl TableRepair for RepairVersions { impl TableRepair for RepairVersions {
type T = VersionTable; type T = VersionTable;
@ -221,7 +220,6 @@ impl TableRepair for RepairVersions {
struct RepairBlockRefs; struct RepairBlockRefs;
#[async_trait]
impl TableRepair for RepairBlockRefs { impl TableRepair for RepairBlockRefs {
type T = BlockRefTable; type T = BlockRefTable;
@ -257,7 +255,6 @@ impl TableRepair for RepairBlockRefs {
struct RepairMpu; struct RepairMpu;
#[async_trait]
impl TableRepair for RepairMpu { impl TableRepair for RepairMpu {
type T = MultipartUploadTable; type T = MultipartUploadTable;

View file

@ -6,13 +6,13 @@ use garage_util::background::*;
use garage_util::config::*; use garage_util::config::*;
use garage_util::error::Error; use garage_util::error::Error;
use garage_api::admin::api_server::AdminApiServer; use garage_api_admin::api_server::AdminApiServer;
use garage_api::s3::api_server::S3ApiServer; use garage_api_s3::api_server::S3ApiServer;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_web::WebServer; use garage_web::WebServer;
#[cfg(feature = "k2v")] #[cfg(feature = "k2v")]
use garage_api::k2v::api_server::K2VApiServer; use garage_api_k2v::api_server::K2VApiServer;
use crate::admin::*; use crate::admin::*;
use crate::secrets::{fill_secrets, Secrets}; use crate::secrets::{fill_secrets, Secrets};

View file

@ -15,7 +15,7 @@ use hyper_util::client::legacy::{connect::HttpConnector, Client};
use hyper_util::rt::TokioExecutor; use hyper_util::rt::TokioExecutor;
use super::garage::{Instance, Key}; use super::garage::{Instance, Key};
use garage_api::signature; use garage_api_common::signature;
pub type Body = FullBody<hyper::body::Bytes>; pub type Body = FullBody<hyper::body::Bytes>;

View file

@ -29,12 +29,11 @@ tokio.workspace = true
# cli deps # cli deps
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
format_table = { workspace = true, optional = true } format_table = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true }
[features] [features]
cli = ["clap", "tokio/fs", "tokio/io-std", "tracing", "tracing-subscriber", "format_table"] cli = ["clap", "tokio/fs", "tokio/io-std", "tracing-subscriber", "format_table"]
[lib] [lib]
path = "lib.rs" path = "lib.rs"

View file

@ -22,7 +22,6 @@ garage_util.workspace = true
garage_net.workspace = true garage_net.workspace = true
async-trait.workspace = true async-trait.workspace = true
arc-swap.workspace = true
blake2.workspace = true blake2.workspace = true
chrono.workspace = true chrono.workspace = true
err-derive.workspace = true err-derive.workspace = true
@ -38,9 +37,7 @@ serde.workspace = true
serde_bytes.workspace = true serde_bytes.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true
tokio.workspace = true tokio.workspace = true
opentelemetry.workspace = true
[features] [features]
default = [ "lmdb", "sqlite" ] default = [ "lmdb", "sqlite" ]

View file

@ -16,8 +16,6 @@ use serde::{Deserialize, Serialize};
use garage_util::data::*; use garage_util::data::*;
use crate::helper::error::{Error as HelperError, OkOrBadRequest};
/// Node IDs used in K2V are u64 integers that are the abbreviation /// Node IDs used in K2V are u64 integers that are the abbreviation
/// of full Garage node IDs which are 256-bit UUIDs. /// of full Garage node IDs which are 256-bit UUIDs.
pub type K2VNodeId = u64; pub type K2VNodeId = u64;
@ -99,10 +97,6 @@ impl CausalContext {
Some(ret) Some(ret)
} }
pub fn parse_helper(s: &str) -> Result<Self, HelperError> {
Self::parse(s).ok_or_bad_request("Invalid causality token")
}
/// Check if this causal context contains newer items than another one /// Check if this causal context contains newer items than another one
pub fn is_newer_than(&self, other: &Self) -> bool { pub fn is_newer_than(&self, other: &Self) -> bool {
vclock_gt(&self.vector_clock, &other.vector_clock) vclock_gt(&self.vector_clock, &other.vector_clock)

View file

@ -10,7 +10,6 @@ use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use async_trait::async_trait;
use futures::stream::FuturesUnordered; use futures::stream::FuturesUnordered;
use futures::StreamExt; use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -537,7 +536,6 @@ impl K2VRpcHandler {
} }
} }
#[async_trait]
impl EndpointHandler<K2VRpc> for K2VRpcHandler { impl EndpointHandler<K2VRpc> for K2VRpcHandler {
async fn handle(self: &Arc<Self>, message: &K2VRpc, _from: NodeID) -> Result<K2VRpc, Error> { async fn handle(self: &Arc<Self>, message: &K2VRpc, _from: NodeID) -> Result<K2VRpc, Error> {
match message { match message {

View file

@ -30,7 +30,6 @@ rand.workspace = true
log.workspace = true log.workspace = true
arc-swap.workspace = true arc-swap.workspace = true
async-trait.workspace = true
err-derive.workspace = true err-derive.workspace = true
bytes.workspace = true bytes.workspace = true
cfg-if.workspace = true cfg-if.workspace = true

View file

@ -6,7 +6,6 @@ use std::sync::{Arc, Mutex};
use std::task::Poll; use std::task::Poll;
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use log::{debug, error, trace}; use log::{debug, error, trace};
@ -220,7 +219,6 @@ impl ClientConn {
impl SendLoop for ClientConn {} impl SendLoop for ClientConn {}
#[async_trait]
impl RecvLoop for ClientConn { impl RecvLoop for ClientConn {
fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream) { fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream) {
trace!("ClientConn recv_handler {}", id); trace!("ClientConn recv_handler {}", id);

View file

@ -1,8 +1,9 @@
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use async_trait::async_trait; use futures::future::{BoxFuture, FutureExt};
use crate::error::Error; use crate::error::Error;
use crate::message::*; use crate::message::*;
@ -14,19 +15,17 @@ use crate::netapp::*;
/// attached to the response.. /// attached to the response..
/// ///
/// The handler object should be in an Arc, see `Endpoint::set_handler` /// The handler object should be in an Arc, see `Endpoint::set_handler`
#[async_trait]
pub trait StreamingEndpointHandler<M>: Send + Sync pub trait StreamingEndpointHandler<M>: Send + Sync
where where
M: Message, M: Message,
{ {
async fn handle(self: &Arc<Self>, m: Req<M>, from: NodeID) -> Resp<M>; fn handle(self: &Arc<Self>, m: Req<M>, from: NodeID) -> impl Future<Output = Resp<M>> + Send;
} }
/// If one simply wants to use an endpoint in a client fashion, /// If one simply wants to use an endpoint in a client fashion,
/// without locally serving requests to that endpoint, /// without locally serving requests to that endpoint,
/// use the unit type `()` as the handler type: /// use the unit type `()` as the handler type:
/// it will panic if it is ever made to handle request. /// it will panic if it is ever made to handle request.
#[async_trait]
impl<M: Message> EndpointHandler<M> for () { impl<M: Message> EndpointHandler<M> for () {
async fn handle(self: &Arc<()>, _m: &M, _from: NodeID) -> M::Response { async fn handle(self: &Arc<()>, _m: &M, _from: NodeID) -> M::Response {
panic!("This endpoint should not have a local handler."); panic!("This endpoint should not have a local handler.");
@ -38,15 +37,13 @@ impl<M: Message> EndpointHandler<M> for () {
/// This trait should be implemented by an object of your application /// This trait should be implemented by an object of your application
/// that can handle a message of type `M`, in the cases where it doesn't /// that can handle a message of type `M`, in the cases where it doesn't
/// care about attached stream in the request nor in the response. /// care about attached stream in the request nor in the response.
#[async_trait]
pub trait EndpointHandler<M>: Send + Sync pub trait EndpointHandler<M>: Send + Sync
where where
M: Message, M: Message,
{ {
async fn handle(self: &Arc<Self>, m: &M, from: NodeID) -> M::Response; fn handle(self: &Arc<Self>, m: &M, from: NodeID) -> impl Future<Output = M::Response> + Send;
} }
#[async_trait]
impl<T, M> StreamingEndpointHandler<M> for T impl<T, M> StreamingEndpointHandler<M> for T
where where
T: EndpointHandler<M>, T: EndpointHandler<M>,
@ -161,9 +158,8 @@ where
pub(crate) type DynEndpoint = Box<dyn GenericEndpoint + Send + Sync>; pub(crate) type DynEndpoint = Box<dyn GenericEndpoint + Send + Sync>;
#[async_trait]
pub(crate) trait GenericEndpoint { pub(crate) trait GenericEndpoint {
async fn handle(&self, req_enc: ReqEnc, from: NodeID) -> Result<RespEnc, Error>; fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<Result<RespEnc, Error>>;
fn drop_handler(&self); fn drop_handler(&self);
fn clone_endpoint(&self) -> DynEndpoint; fn clone_endpoint(&self) -> DynEndpoint;
} }
@ -174,13 +170,13 @@ where
M: Message, M: Message,
H: StreamingEndpointHandler<M>; H: StreamingEndpointHandler<M>;
#[async_trait]
impl<M, H> GenericEndpoint for EndpointArc<M, H> impl<M, H> GenericEndpoint for EndpointArc<M, H>
where where
M: Message, M: Message,
H: StreamingEndpointHandler<M> + 'static, H: StreamingEndpointHandler<M> + 'static,
{ {
async fn handle(&self, req_enc: ReqEnc, from: NodeID) -> Result<RespEnc, Error> { fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<Result<RespEnc, Error>> {
async move {
match self.0.handler.load_full() { match self.0.handler.load_full() {
None => Err(Error::NoHandler), None => Err(Error::NoHandler),
Some(h) => { Some(h) => {
@ -190,6 +186,8 @@ where
} }
} }
} }
.boxed()
}
fn drop_handler(&self) { fn drop_handler(&self) {
self.0.handler.swap(None); self.0.handler.swap(None);

View file

@ -5,7 +5,6 @@ use std::sync::{Arc, RwLock};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::auth; use sodiumoxide::crypto::auth;
@ -457,7 +456,6 @@ impl NetApp {
} }
} }
#[async_trait]
impl EndpointHandler<HelloMessage> for NetApp { impl EndpointHandler<HelloMessage> for NetApp {
async fn handle(self: &Arc<Self>, msg: &HelloMessage, from: NodeID) { async fn handle(self: &Arc<Self>, msg: &HelloMessage, from: NodeID) {
debug!("Hello from {:?}: {:?}", hex::encode(&from[..8]), msg); debug!("Hello from {:?}: {:?}", hex::encode(&from[..8]), msg);

View file

@ -5,7 +5,6 @@ use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait;
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -592,7 +591,6 @@ impl PeeringManager {
} }
} }
#[async_trait]
impl EndpointHandler<PingMessage> for PeeringManager { impl EndpointHandler<PingMessage> for PeeringManager {
async fn handle(self: &Arc<Self>, ping: &PingMessage, from: NodeID) -> PingMessage { async fn handle(self: &Arc<Self>, ping: &PingMessage, from: NodeID) -> PingMessage {
let ping_resp = PingMessage { let ping_resp = PingMessage {
@ -604,7 +602,6 @@ impl EndpointHandler<PingMessage> for PeeringManager {
} }
} }
#[async_trait]
impl EndpointHandler<PeerListMessage> for PeeringManager { impl EndpointHandler<PeerListMessage> for PeeringManager {
async fn handle( async fn handle(
self: &Arc<Self>, self: &Arc<Self>,

View file

@ -1,7 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use log::*; use log::*;
@ -50,7 +49,6 @@ impl Drop for Sender {
/// according to the protocol defined above: chunks of message in progress of being /// according to the protocol defined above: chunks of message in progress of being
/// received are stored in a buffer, and when the last chunk of a message is received, /// received are stored in a buffer, and when the last chunk of a message is received,
/// the full message is passed to the receive handler. /// the full message is passed to the receive handler.
#[async_trait]
pub(crate) trait RecvLoop: Sync + 'static { pub(crate) trait RecvLoop: Sync + 'static {
fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream); fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream);
fn cancel_handler(self: &Arc<Self>, _id: RequestID) {} fn cancel_handler(self: &Arc<Self>, _id: RequestID) {}

View file

@ -3,7 +3,6 @@ use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use async_trait::async_trait;
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use log::*; use log::*;
@ -273,7 +272,6 @@ impl DataFrame {
/// ///
/// The `.send_loop()` exits when the sending end of the channel is closed, /// The `.send_loop()` exits when the sending end of the channel is closed,
/// or if there is an error at any time writing to the async writer. /// or if there is an error at any time writing to the async writer.
#[async_trait]
pub(crate) trait SendLoop: Sync { pub(crate) trait SendLoop: Sync {
async fn send_loop<W>( async fn send_loop<W>(
self: Arc<Self>, self: Arc<Self>,

View file

@ -3,7 +3,6 @@ use std::net::SocketAddr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use log::*; use log::*;
use futures::io::{AsyncReadExt, AsyncWriteExt}; use futures::io::{AsyncReadExt, AsyncWriteExt};
@ -174,7 +173,6 @@ impl ServerConn {
impl SendLoop for ServerConn {} impl SendLoop for ServerConn {}
#[async_trait]
impl RecvLoop for ServerConn { impl RecvLoop for ServerConn {
fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream) { fn recv_handler(self: &Arc<Self>, id: RequestID, stream: ByteStream) {
let resp_send = match self.resp_send.load_full() { let resp_send = match self.resp_send.load_full() {

View file

@ -15,12 +15,10 @@ path = "lib.rs"
[dependencies] [dependencies]
format_table.workspace = true format_table.workspace = true
garage_db.workspace = true
garage_util.workspace = true garage_util.workspace = true
garage_net.workspace = true garage_net.workspace = true
arc-swap.workspace = true arc-swap.workspace = true
bytes.workspace = true
bytesize.workspace = true bytesize.workspace = true
gethostname.workspace = true gethostname.workspace = true
hex.workspace = true hex.workspace = true
@ -46,9 +44,7 @@ reqwest = { workspace = true, optional = true }
pnet_datalink.workspace = true pnet_datalink.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true
tokio.workspace = true tokio.workspace = true
tokio-stream.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
[features] [features]

View file

@ -7,7 +7,6 @@ use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use futures::join; use futures::join;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::sign::ed25519; use sodiumoxide::crypto::sign::ed25519;
@ -749,7 +748,6 @@ impl System {
} }
} }
#[async_trait]
impl EndpointHandler<SystemRpc> for System { impl EndpointHandler<SystemRpc> for System {
async fn handle(self: &Arc<Self>, msg: &SystemRpc, from: NodeID) -> Result<SystemRpc, Error> { async fn handle(self: &Arc<Self>, msg: &SystemRpc, from: NodeID) -> Result<SystemRpc, Error> {
match msg { match msg {

View file

@ -22,7 +22,6 @@ opentelemetry.workspace = true
async-trait.workspace = true async-trait.workspace = true
arc-swap.workspace = true arc-swap.workspace = true
bytes.workspace = true
hex.workspace = true hex.workspace = true
hexdump.workspace = true hexdump.workspace = true
tracing.workspace = true tracing.workspace = true

View file

@ -4,6 +4,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf; use serde_bytes::ByteBuf;
@ -272,7 +273,6 @@ impl<F: TableSchema, R: TableReplication> TableGc<F, R> {
} }
} }
#[async_trait]
impl<F: TableSchema, R: TableReplication> EndpointHandler<GcRpc> for TableGc<F, R> { impl<F: TableSchema, R: TableReplication> EndpointHandler<GcRpc> for TableGc<F, R> {
async fn handle(self: &Arc<Self>, message: &GcRpc, _from: NodeID) -> Result<GcRpc, Error> { async fn handle(self: &Arc<Self>, message: &GcRpc, _from: NodeID) -> Result<GcRpc, Error> {
match message { match message {

View file

@ -444,7 +444,6 @@ impl<F: TableSchema, R: TableReplication> TableSyncer<F, R> {
// ======= SYNCHRONIZATION PROCEDURE -- RECEIVER SIDE ====== // ======= SYNCHRONIZATION PROCEDURE -- RECEIVER SIDE ======
#[async_trait]
impl<F: TableSchema, R: TableReplication> EndpointHandler<SyncRpc> for TableSyncer<F, R> { impl<F: TableSchema, R: TableReplication> EndpointHandler<SyncRpc> for TableSyncer<F, R> {
async fn handle(self: &Arc<Self>, message: &SyncRpc, from: NodeID) -> Result<SyncRpc, Error> { async fn handle(self: &Arc<Self>, message: &SyncRpc, from: NodeID) -> Result<SyncRpc, Error> {
match message { match message {

View file

@ -2,7 +2,6 @@ use std::borrow::Borrow;
use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use futures::stream::*; use futures::stream::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf; use serde_bytes::ByteBuf;
@ -204,6 +203,10 @@ impl<F: TableSchema, R: TableReplication> Table<F, R> {
entries_vec.push((write_sets, e_enc)); entries_vec.push((write_sets, e_enc));
} }
if entries_vec.is_empty() {
return Ok(());
}
// Compute a deduplicated list of all of the write sets, // Compute a deduplicated list of all of the write sets,
// and compute an index from each node to the position of the sets in which // and compute an index from each node to the position of the sets in which
// it takes part, to optimize the detection of a quorum. // it takes part, to optimize the detection of a quorum.
@ -496,7 +499,6 @@ impl<F: TableSchema, R: TableReplication> Table<F, R> {
} }
} }
#[async_trait]
impl<F: TableSchema, R: TableReplication> EndpointHandler<TableRpc<F>> for Table<F, R> { impl<F: TableSchema, R: TableReplication> EndpointHandler<TableRpc<F>> for Table<F, R> {
async fn handle( async fn handle(
self: &Arc<Self>, self: &Arc<Self>,

View file

@ -20,9 +20,7 @@ garage_net.workspace = true
arc-swap.workspace = true arc-swap.workspace = true
async-trait.workspace = true async-trait.workspace = true
blake2.workspace = true blake2.workspace = true
bytes.workspace = true
bytesize.workspace = true bytesize.workspace = true
digest.workspace = true
err-derive.workspace = true err-derive.workspace = true
hexdump.workspace = true hexdump.workspace = true
xxhash-rust.workspace = true xxhash-rust.workspace = true

View file

@ -14,7 +14,8 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_api.workspace = true garage_api_common.workspace = true
garage_api_s3.workspace = true
garage_model.workspace = true garage_model.workspace = true
garage_util.workspace = true garage_util.workspace = true
garage_table.workspace = true garage_table.workspace = true
@ -23,12 +24,9 @@ err-derive.workspace = true
tracing.workspace = true tracing.workspace = true
percent-encoding.workspace = true percent-encoding.workspace = true
futures.workspace = true
http.workspace = true http.workspace = true
http-body-util.workspace = true http-body-util.workspace = true
hyper.workspace = true hyper.workspace = true
hyper-util.workspace = true
tokio.workspace = true tokio.workspace = true

View file

@ -2,14 +2,14 @@ use err_derive::Error;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::{HeaderMap, StatusCode}; use hyper::{HeaderMap, StatusCode};
use garage_api::generic_server::ApiError; use garage_api_common::generic_server::ApiError;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
/// An error received from the API crate /// An error received from the API crate
#[error(display = "API error: {}", _0)] #[error(display = "API error: {}", _0)]
ApiError(garage_api::s3::error::Error), ApiError(garage_api_s3::error::Error),
/// The file does not exist /// The file does not exist
#[error(display = "Not found")] #[error(display = "Not found")]
@ -22,10 +22,10 @@ pub enum Error {
impl<T> From<T> for Error impl<T> From<T> for Error
where where
garage_api::s3::error::Error: From<T>, garage_api_s3::error::Error: From<T>,
{ {
fn from(err: T) -> Self { fn from(err: T) -> Self {
Error::ApiError(garage_api::s3::error::Error::from(err)) Error::ApiError(garage_api_s3::error::Error::from(err))
} }
} }

View file

@ -20,13 +20,15 @@ use opentelemetry::{
use crate::error::*; use crate::error::*;
use garage_api::generic_server::{server_loop, UnixListenerOn}; use garage_api_common::cors::{
use garage_api::helpers::*; add_cors_headers, find_matching_cors_rule, handle_options_for_bucket,
use garage_api::s3::cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket}; };
use garage_api::s3::error::{ use garage_api_common::generic_server::{server_loop, UnixListenerOn};
use garage_api_common::helpers::*;
use garage_api_s3::error::{
CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError, CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError,
}; };
use garage_api::s3::get::{handle_get_without_ctx, handle_head_without_ctx}; use garage_api_s3::get::{handle_get_without_ctx, handle_head_without_ctx};
use garage_model::garage::Garage; use garage_model::garage::Garage;