mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-13 15:46:53 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8811f7c9d | |||
| 2090a6187f | |||
| 6f13d083ab | |||
| 8c4f418fe8 | |||
| bef6d627b0 | |||
| e93d7fb228 | |||
| eaf54efb25 | |||
| 93f8d59e4c | |||
| cc1caa87fb | |||
| 69b89fb46d | |||
| 6b47c294f5 | |||
| 28c015d9ff | |||
| 4e8af1d956 | |||
| 3e7f766d95 | |||
| 43e13a501d | |||
| ada7899b24 | |||
| b2c51844a1 |
+169
-285
@@ -36,7 +36,7 @@ steps:
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-shell --arg release false --run "cargo fmt -- --check"
|
||||
- nix-shell --arg release false --run "cargo clippy -- --allow clippy::needless_borrow --allow clippy::needless-return --deny warnings"
|
||||
- nix-shell --arg release false --run "cargo clippy -- --deny warnings"
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
@@ -80,38 +80,6 @@ steps:
|
||||
- nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
- nix-shell --arg release false --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-build --no-out-link shell.nix --arg release false -A inputDerivation))
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target x86_64-unknown-linux-musl --argstr compileMode test))
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target x86_64-unknown-linux-musl --arg release false))
|
||||
when:
|
||||
event:
|
||||
- cron
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
@@ -212,27 +180,6 @@ steps:
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
@@ -276,115 +223,94 @@ trigger:
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
# ---
|
||||
# kind: pipeline
|
||||
# type: docker
|
||||
# name: release-linux-i686
|
||||
#
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# host:
|
||||
# path: /var/lib/drone/nix
|
||||
# - name: nix_config
|
||||
# temp: {}
|
||||
#
|
||||
# environment:
|
||||
# TARGET: i686-unknown-linux-musl
|
||||
#
|
||||
# steps:
|
||||
# - name: setup nix
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - cp nix/nix.conf /etc/nix/nix.conf
|
||||
# - nix-build --no-build-output --no-out-link shell.nix -A inputDerivation
|
||||
#
|
||||
# - name: build
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
#
|
||||
# - name: integration
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
#
|
||||
# - name: update cache
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: cache_aws_secret_access_key
|
||||
# NIX_PRIV_KEY:
|
||||
# from_secret: nix_priv_key
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
# - |
|
||||
# nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
# $(nix-store -qR --include-outputs \
|
||||
# $(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
#
|
||||
# - name: push static binary
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: garagehq_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: garagehq_aws_secret_access_key
|
||||
# commands:
|
||||
# - nix-shell --arg rust false --arg integration false --run "to_s3"
|
||||
#
|
||||
# - name: docker build and publish
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# environment:
|
||||
# DOCKER_AUTH:
|
||||
# from_secret: docker_auth
|
||||
# DOCKER_PLATFORM: "linux/386"
|
||||
# CONTAINER_NAME: "dxflrs/386_garage"
|
||||
# HOME: "/kaniko"
|
||||
# commands:
|
||||
# - mkdir -p /kaniko/.docker
|
||||
# - echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
# - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
# - nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
#
|
||||
# trigger:
|
||||
# event:
|
||||
# - promote
|
||||
# - cron
|
||||
#
|
||||
# node:
|
||||
# nix: 1
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-i686
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: i686-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link shell.nix -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: integration
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
commands:
|
||||
- nix-shell --arg rust false --arg integration false --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
DOCKER_PLATFORM: "linux/386"
|
||||
CONTAINER_NAME: "dxflrs/386_garage"
|
||||
HOME: "/kaniko"
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
@@ -423,27 +349,6 @@ steps:
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
@@ -486,105 +391,84 @@ trigger:
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
# ---
|
||||
# kind: pipeline
|
||||
# type: docker
|
||||
# name: release-linux-armv6l
|
||||
#
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# host:
|
||||
# path: /var/lib/drone/nix
|
||||
# - name: nix_config
|
||||
# temp: {}
|
||||
#
|
||||
# environment:
|
||||
# TARGET: armv6l-unknown-linux-musleabihf
|
||||
#
|
||||
# steps:
|
||||
# - name: setup nix
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - cp nix/nix.conf /etc/nix/nix.conf
|
||||
# - nix-build --no-build-output --no-out-link --arg rust false --arg integration false -A inputDerivation
|
||||
#
|
||||
# - name: build
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
#
|
||||
# - name: update cache
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: cache_aws_secret_access_key
|
||||
# NIX_PRIV_KEY:
|
||||
# from_secret: nix_priv_key
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# commands:
|
||||
# - (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
# - |
|
||||
# nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
# $(nix-store -qR --include-outputs \
|
||||
# $(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
#
|
||||
# - name: push static binary
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: garagehq_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: garagehq_aws_secret_access_key
|
||||
# commands:
|
||||
# - nix-shell --arg integration false --arg rust false --run "to_s3"
|
||||
#
|
||||
# - name: docker build and publish
|
||||
# image: nixpkgs/nix:nixos-21.05
|
||||
# volumes:
|
||||
# - name: nix_store
|
||||
# path: /nix
|
||||
# - name: nix_config
|
||||
# path: /etc/nix
|
||||
# environment:
|
||||
# DOCKER_AUTH:
|
||||
# from_secret: docker_auth
|
||||
# DOCKER_PLATFORM: "linux/arm"
|
||||
# CONTAINER_NAME: "dxflrs/arm_garage"
|
||||
# HOME: "/kaniko"
|
||||
# commands:
|
||||
# - mkdir -p /kaniko/.docker
|
||||
# - echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
# - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
# - nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
#
|
||||
# trigger:
|
||||
# event:
|
||||
# - promote
|
||||
# - cron
|
||||
#
|
||||
# node:
|
||||
# nix: 1
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-armv6l
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: armv6l-unknown-linux-musleabihf
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link --arg rust false --arg integration false -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
commands:
|
||||
- nix-shell --arg integration false --arg rust false --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
DOCKER_PLATFORM: "linux/arm"
|
||||
CONTAINER_NAME: "dxflrs/arm_garage"
|
||||
HOME: "/kaniko"
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
@@ -613,9 +497,9 @@ steps:
|
||||
|
||||
depends_on:
|
||||
- release-linux-x86_64
|
||||
#- release-linux-i686
|
||||
- release-linux-i686
|
||||
- release-linux-aarch64
|
||||
#- release-linux-armv6l
|
||||
- release-linux-armv6l
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
||||
Generated
+2
-1
@@ -874,7 +874,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "netapp"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.deuxfleurs.fr/lx/netapp#c20d36892bcccae580603249706ba60d54a46d7f"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d202ff23744499a8e5d89a3199ca7b9a8aef9980a23b1b09d6508a8db2a118d2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
||||
@@ -626,7 +626,7 @@ in
|
||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
@@ -687,7 +687,7 @@ in
|
||||
garage_util = rustPackages."unknown".garage_util."0.4.0" { inherit profileName; };
|
||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -714,7 +714,7 @@ in
|
||||
hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.13" { inherit profileName; };
|
||||
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -761,7 +761,7 @@ in
|
||||
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.5" { inherit profileName; };
|
||||
hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.13" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -1228,15 +1228,11 @@ in
|
||||
};
|
||||
});
|
||||
|
||||
"git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" = overridableMkRustCrate (profileName: rec {
|
||||
"registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" = overridableMkRustCrate (profileName: rec {
|
||||
name = "netapp";
|
||||
version = "0.3.0";
|
||||
registry = "git+https://git.deuxfleurs.fr/lx/netapp";
|
||||
src = fetchCrateGit {
|
||||
url = https://git.deuxfleurs.fr/lx/netapp;
|
||||
name = "netapp";
|
||||
version = "0.3.0";
|
||||
rev = "9b64c27da68f7ac9049e02e26da918e871a63f07";};
|
||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
src = fetchCratesIo { inherit name version; sha256 = "d202ff23744499a8e5d89a3199ca7b9a8aef9980a23b1b09d6508a8db2a118d2"; };
|
||||
features = builtins.concatLists [
|
||||
[ "default" ]
|
||||
];
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ ENV RUST_BACKTRACE=1
|
||||
ENV RUST_LOG=garage=info
|
||||
|
||||
COPY result/bin/garage /
|
||||
CMD [ "/garage", "server", "-c", "config.toml"]
|
||||
CMD [ "/garage", "server"]
|
||||
|
||||
+2
-11
@@ -11,26 +11,17 @@ with import ./nix/common.nix;
|
||||
let
|
||||
crossSystem = { config = target; };
|
||||
in let
|
||||
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay rustOverlay ];
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
rustDist = pkgs.buildPackages.rust-bin.stable.latest.default;
|
||||
|
||||
/*
|
||||
The following complexity should be abstracted by makePackageSet' (note the final quote).
|
||||
However its code uses deprecated features of rust-overlay that can lead to bug.
|
||||
Instead, we build our own rustChannel object with the recommended API of rust-overlay.
|
||||
*/
|
||||
rustChannel = rustDist // {
|
||||
cargo = rustDist;
|
||||
rustc = rustDist.override {
|
||||
targets = [
|
||||
(pkgs.rustBuilder.rustLib.realHostTriple pkgs.stdenv.targetPlatform)
|
||||
];
|
||||
};
|
||||
};
|
||||
rustChannel = pkgs.rustPlatform.rust;
|
||||
|
||||
overrides = pkgs.buildPackages.rustBuilder.overrides.all ++ [
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
|
||||
+12
-7
@@ -5,14 +5,19 @@
|
||||
- [Quick start](./quick_start/index.md)
|
||||
|
||||
- [Cookbook](./cookbook/index.md)
|
||||
- [Deploying Garage](./cookbook/real_world.md)
|
||||
- [Configuring S3 clients](./cookbook/clients.md)
|
||||
- [Hosting a website](./cookbook/website.md)
|
||||
- [Recovering from failures](./cookbook/recovering.md)
|
||||
- [Building from source](./cookbook/from_source.md)
|
||||
- [Starting with Systemd](./cookbook/systemd.md)
|
||||
- [Integrate as a media backend]()
|
||||
- [Operate a cluster]()
|
||||
- [Integration with systemd](./cookbook/systemd.md)
|
||||
- [Configuring a reverse proxy](./cookbook/reverse_proxy.md)
|
||||
- [Production Deployment](./cookbook/real_world.md)
|
||||
- [Recovering from failures](./cookbook/recovering.md)
|
||||
|
||||
- [Integrations](./connect/index.md)
|
||||
- [Apps (Nextcloud, Peertube...)](./connect/apps.md)
|
||||
- [Websites (Hugo, Jekyll, Publii...)](./connect/websites.md)
|
||||
- [Repositories (Docker, Nix...)](./connect/repositories.md)
|
||||
- [CLI tools (rclone, awscli, mc...)](./connect/cli.md)
|
||||
- [Your code (PHP, JS, Go...)](./connect/code.md)
|
||||
|
||||
|
||||
- [Reference Manual](./reference_manual/index.md)
|
||||
- [Garage configuration file](./reference_manual/configuration.md)
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
# Apps (Nextcloud, Peertube...)
|
||||
|
||||
In this section, we cover the following software: [Nextcloud](#nextcloud), [Peertube](#peertube), [Mastodon](#mastodon), [Matrix](#matrix)
|
||||
|
||||
## Nextcloud
|
||||
|
||||
Nextcloud is a popular file synchronisation and backup service.
|
||||
By default, Nextcloud stores its data on the local filesystem.
|
||||
If you want to expand your storage to aggregate multiple servers, Garage is the way to go.
|
||||
|
||||
A S3 backend can be configured in two ways on Nextcloud, either as Primary Storage or as an External Storage.
|
||||
Primary storage will store all your data on S3, in an opaque manner, and will provide the best performances.
|
||||
External storage enable you to select which data will be stored on S3, your file hierarchy will be preserved in S3, but it might be slower.
|
||||
|
||||
In the following, we cover both methods but before reading our guide, we suppose you have done some preliminary steps.
|
||||
First, we expect you have an already installed and configured Nextcloud instance.
|
||||
Second, we suppose you have created a key and a bucket.
|
||||
|
||||
As a reminder, you can create a key for your nextcloud instance as follow:
|
||||
|
||||
```bash
|
||||
garage key new --name nextcloud-key
|
||||
```
|
||||
|
||||
Keep the Key ID and the Secret key in a pad, they will be needed later.
|
||||
Then you can create a bucket and give read/write rights to your key on this bucket with:
|
||||
|
||||
```bash
|
||||
garage bucket create nextcloud
|
||||
garage bucket allow nextcloud --read --write --key nextcloud-key
|
||||
```
|
||||
|
||||
|
||||
### Primary Storage
|
||||
|
||||
Now edit your Nextcloud configuration file to enable object storage.
|
||||
On my installation, the config. file is located at the following path: `/var/www/nextcloud/config/config.php`.
|
||||
We will add a new root key to the `$CONFIG` dictionnary named `objectstore`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$CONFIG = array(
|
||||
/* your existing configuration */
|
||||
'objectstore' => [
|
||||
'class' => '\\OC\\Files\\ObjectStore\\S3',
|
||||
'arguments' => [
|
||||
'bucket' => 'nextcloud', // Your bucket name, must be created before
|
||||
'autocreate' => false, // Garage does not support autocreate
|
||||
'key' => 'xxxxxxxxx', // The Key ID generated previously
|
||||
'secret' => 'xxxxxxxxx', // The Secret key generated previously
|
||||
'hostname' => '127.0.0.1', // Can also be a domain name, eg. garage.example.com
|
||||
'port' => 3900, // Put your reverse proxy port or your S3 API port
|
||||
'use_ssl' => false, // Set it to true if you have a TLS enabled reverse proxy
|
||||
'region' => 'garage', // Garage has only one region named "garage"
|
||||
'use_path_style' => true // Garage supports only path style, must be set to true
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
That's all, your Nextcloud will store all your data to S3.
|
||||
To test your new configuration, just reload your Nextcloud webpage and start sending data.
|
||||
|
||||
*External link:* [Nextcloud Documentation > Primary Storage](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/primary_storage.html)
|
||||
|
||||
### External Storage
|
||||
|
||||
**From the GUI.** Activate the "External storage support" app from the "Applications" page (click on your account icon on the top right corner of your screen to display the menu). Go to your parameters page (also located below your account icon). Click on external storage (or the corresponding translation in your language).
|
||||
|
||||
[](./cli-nextcloud-gui.png)
|
||||
*Click on the picture to zoom*
|
||||
|
||||
Add a new external storage. Put what you want in "folder name" (eg. "shared"). Select "Amazon S3". Keep "Access Key" for the Authentication field.
|
||||
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region (garage). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
|
||||
|
||||
Now go to your "Files" app and a new "linked folder" has appeared with the name you chose earlier (eg. "shared").
|
||||
|
||||
*External link:* [Nextcloud Documentation > External Storage Configuration GUI](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/external_storage_configuration_gui.html)
|
||||
|
||||
**From the CLI.** First install the external storage application:
|
||||
|
||||
```bash
|
||||
php occ app:install files_external
|
||||
```
|
||||
|
||||
Then add a new mount point with:
|
||||
|
||||
```bash
|
||||
php occ files_external:create \
|
||||
-c bucket=nextcloud \
|
||||
-c hostname=127.0.0.1 \
|
||||
-c port=3900 \
|
||||
-c region=garage \
|
||||
-c use_ssl=false \
|
||||
-c use_path_style=true \
|
||||
-c legacy_auth=false \
|
||||
-c key=GKxxxx \
|
||||
-c secret=xxxx \
|
||||
shared amazons3 amazons3::accesskey
|
||||
```
|
||||
|
||||
Adapt the `hostname`, `port`, `use_ssl`, `key`, and `secret` entries to your configuration.
|
||||
Do not change the `use_path_style` and `legacy_auth` entries, other configurations are not supported.
|
||||
|
||||
*External link:* [Nextcloud Documentation > occ command > files external](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html#files-external-label)
|
||||
|
||||
|
||||
## Peertube
|
||||
|
||||
Peertube proposes a clever integration of S3 by directly exposing its endpoint instead of proxifying requests through the application.
|
||||
In other words, Peertube is only responsible of the "control plane" and offload the "data plane" to Garage.
|
||||
In return, this system is a bit harder to configure, especially with Garage that supports less feature than other older S3 backends.
|
||||
We show that it is still possible to configure Garage with Peertube, allowing you to spread the load and the bandiwdth usage on the Garage cluster.
|
||||
|
||||
### Enable path-style access by patching Peertube
|
||||
|
||||
First, you will need to apply a small patch on Peertube ([#4510](https://github.com/Chocobozzz/PeerTube/pull/4510)):
|
||||
|
||||
```diff
|
||||
From e3b4c641bdf67e07d406a1d49d6aa6b1fbce2ab4 Mon Sep 17 00:00:00 2001
|
||||
From: Martin Honermeyer <maze@strahlungsfrei.de>
|
||||
Date: Sun, 31 Oct 2021 12:34:04 +0100
|
||||
Subject: [PATCH] Allow setting path-style access for object storage
|
||||
|
||||
---
|
||||
config/default.yaml | 4 ++++
|
||||
config/production.yaml.example | 4 ++++
|
||||
server/initializers/config.ts | 1 +
|
||||
server/lib/object-storage/shared/client.ts | 3 ++-
|
||||
.../production/config/custom-environment-variables.yaml | 2 ++
|
||||
5 files changed, 13 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/config/default.yaml b/config/default.yaml
|
||||
index cf9d69a6211..4efd56fb804 100644
|
||||
--- a/config/default.yaml
|
||||
+++ b/config/default.yaml
|
||||
@@ -123,6 +123,10 @@ object_storage:
|
||||
# You can also use AWS_SECRET_ACCESS_KEY env variable
|
||||
secret_access_key: ''
|
||||
|
||||
+ # Reference buckets via path rather than subdomain
|
||||
+ # (i.e. "my-endpoint.com/bucket" instead of "bucket.my-endpoint.com")
|
||||
+ force_path_style: false
|
||||
+
|
||||
# Maximum amount to upload in one request to object storage
|
||||
max_upload_part: 2GB
|
||||
|
||||
diff --git a/config/production.yaml.example b/config/production.yaml.example
|
||||
index 70993bf57a3..9ca2de5f4c9 100644
|
||||
--- a/config/production.yaml.example
|
||||
+++ b/config/production.yaml.example
|
||||
@@ -121,6 +121,10 @@ object_storage:
|
||||
# You can also use AWS_SECRET_ACCESS_KEY env variable
|
||||
secret_access_key: ''
|
||||
|
||||
+ # Reference buckets via path rather than subdomain
|
||||
+ # (i.e. "my-endpoint.com/bucket" instead of "bucket.my-endpoint.com")
|
||||
+ force_path_style: false
|
||||
+
|
||||
# Maximum amount to upload in one request to object storage
|
||||
max_upload_part: 2GB
|
||||
|
||||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
|
||||
index 8375bf4304c..d726c59a4b6 100644
|
||||
--- a/server/initializers/config.ts
|
||||
+++ b/server/initializers/config.ts
|
||||
@@ -91,6 +91,7 @@ const CONFIG = {
|
||||
ACCESS_KEY_ID: config.get<string>('object_storage.credentials.access_key_id'),
|
||||
SECRET_ACCESS_KEY: config.get<string>('object_storage.credentials.secret_access_key')
|
||||
},
|
||||
+ FORCE_PATH_STYLE: config.get<boolean>('object_storage.force_path_style'),
|
||||
VIDEOS: {
|
||||
BUCKET_NAME: config.get<string>('object_storage.videos.bucket_name'),
|
||||
PREFIX: config.get<string>('object_storage.videos.prefix'),
|
||||
diff --git a/server/lib/object-storage/shared/client.ts b/server/lib/object-storage/shared/client.ts
|
||||
index c9a61459336..eadad02f93f 100644
|
||||
--- a/server/lib/object-storage/shared/client.ts
|
||||
+++ b/server/lib/object-storage/shared/client.ts
|
||||
@@ -26,7 +26,8 @@ function getClient () {
|
||||
accessKeyId: OBJECT_STORAGE.CREDENTIALS.ACCESS_KEY_ID,
|
||||
secretAccessKey: OBJECT_STORAGE.CREDENTIALS.SECRET_ACCESS_KEY
|
||||
}
|
||||
- : undefined
|
||||
+ : undefined,
|
||||
+ forcePathStyle: CONFIG.OBJECT_STORAGE.FORCE_PATH_STYLE
|
||||
})
|
||||
|
||||
logger.info('Initialized S3 client %s with region %s.', getEndpoint(), OBJECT_STORAGE.REGION, lTags())
|
||||
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml
|
||||
index c7cd28e6521..a960bab0bc9 100644
|
||||
--- a/support/docker/production/config/custom-environment-variables.yaml
|
||||
+++ b/support/docker/production/config/custom-environment-variables.yaml
|
||||
@@ -54,6 +54,8 @@ object_storage:
|
||||
|
||||
region: "PEERTUBE_OBJECT_STORAGE_REGION"
|
||||
|
||||
+ force_path_style: "PEERTUBE_OBJECT_STORAGE_FORCE_PATH_STYLE"
|
||||
+
|
||||
max_upload_part:
|
||||
__name: "PEERTUBE_OBJECT_STORAGE_MAX_UPLOAD_PART"
|
||||
__format: "json"
|
||||
```
|
||||
|
||||
You can then recompile it with:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
And it can be started with:
|
||||
|
||||
```
|
||||
NODE_ENV=production NODE_CONFIG_DIR=/srv/peertube/config node dist/server.js
|
||||
```
|
||||
|
||||
|
||||
### Create resources in Garage
|
||||
|
||||
Create a key for Peertube:
|
||||
|
||||
```bash
|
||||
garage key new --name peertube-key
|
||||
```
|
||||
|
||||
Keep the Key ID and the Secret key in a pad, they will be needed later.
|
||||
|
||||
We need two buckets, one for normal videos (named peertube-video) and one for webtorrent videos (named peertube-playlist).
|
||||
```bash
|
||||
garage bucket create peertube-video
|
||||
garage bucket create peertube-playlist
|
||||
```
|
||||
|
||||
Now we allow our key to read and write on these buckets:
|
||||
|
||||
```
|
||||
garage bucket allow peertube-playlist --read --write --key peertube-key
|
||||
garage bucket allow peertube-video --read --write --key peertube-key
|
||||
```
|
||||
|
||||
Finally, we need to expose these buckets publicly to serve their content to users:
|
||||
|
||||
```bash
|
||||
garage bucket website --allow peertube-playlist
|
||||
garage bucket website --allow peertube-video
|
||||
```
|
||||
|
||||
These buckets are now accessible on the web port (by default 3902) with the following URL: `http://<bucket><root_domain>:<web_port>` where the root domain is defined in your configuration file (by default `.web.garage`). So we have currently the following URLs:
|
||||
* http://peertube-playlist.web.garage:3902
|
||||
* http://peertube-video.web.garage:3902
|
||||
|
||||
Make sure you (will) have a corresponding DNS entry for them.
|
||||
|
||||
### Configure a Reverse Proxy to serve CORS
|
||||
|
||||
Now we will configure a reverse proxy in front of Garage.
|
||||
This is required as we have no other way to serve CORS headers yet.
|
||||
For our example, we will use nginx:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
# In production you should use TLS instead of plain HTTP
|
||||
listen [::]:80;
|
||||
|
||||
server_name peertube-video.web.garage peertube-playlist.web.garage;
|
||||
|
||||
location / {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Max-Age 3600;
|
||||
add_header Access-Control-Expose-Headers Content-Length;
|
||||
add_header Access-Control-Allow-Headers Range;
|
||||
|
||||
# We do not forward OPTIONS request to Garage
|
||||
# as it does not know how to interpret them.
|
||||
# Instead, we simply answers 200.
|
||||
if ($request_method !~ ^(GET|HEAD)$ ) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
# If your do not have a Garage instance on the reverse proxy, change the URL here.
|
||||
proxy_pass http://127.0.0.1:3902;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now make sure that your 2 dns entries are pointing to your reverse proxy.
|
||||
|
||||
### Configure Peertube
|
||||
|
||||
You must edit the file named `config/production.yaml`, we are only modifying the root key named `object_storage`:
|
||||
|
||||
```yaml
|
||||
object_storage:
|
||||
enabled: true
|
||||
|
||||
# Put localhost only if you have a garage instance running on that node
|
||||
endpoint: 'http://localhost:3900' # or "garage.example.com" if you have TLS on port 443
|
||||
|
||||
# This entry has been added by our patch, must be set to true
|
||||
force_path_style: true
|
||||
|
||||
# Garage supports only one region for now, named garage
|
||||
region: 'garage'
|
||||
|
||||
credentials:
|
||||
access_key_id: 'GKxxxx'
|
||||
secret_access_key: 'xxxx'
|
||||
|
||||
max_upload_part: 2GB
|
||||
|
||||
streaming_playlists:
|
||||
bucket_name: 'peertube-playlist'
|
||||
|
||||
# Keep it empty for our example
|
||||
prefix: ''
|
||||
|
||||
# You must fill this field to make Peertube use our reverse proxy/website logic
|
||||
base_url: 'http://peertube-playlist.web.garage' # Example: 'https://mirror.example.com'
|
||||
|
||||
# Same settings but for webtorrent videos
|
||||
videos:
|
||||
bucket_name: 'peertube-video'
|
||||
prefix: ''
|
||||
# You must fill this field to make Peertube use our reverse proxy/website logic
|
||||
base_url: 'http://peertube-video.web.garage'
|
||||
```
|
||||
|
||||
### That's all
|
||||
|
||||
Everything must be configured now, simply restart Peertube and try to upload a video.
|
||||
You must see in your browser console that data are fetched directly from our bucket (through the reverse proxy).
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
*Known bug:* The playback does not start and some 400 Bad Request Errors appear in your browser console and on Garage.
|
||||
If the description of the error contains HTTP Invalid Range: InvalidRange, the error is due to a buggy ffmpeg version.
|
||||
You must avoid the 4.4.0 and use either a newer or older version.
|
||||
|
||||
*Associated issues:* [#137](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/137), [#138](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/138), [#140](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/140). These issues are non blocking.
|
||||
|
||||
*External link:* [Peertube Documentation > Remote Storage](https://docs.joinpeertube.org/admin-remote-storage)
|
||||
|
||||
## Mastodon
|
||||
|
||||
https://docs.joinmastodon.org/admin/config/#cdn
|
||||
|
||||
## Matrix
|
||||
|
||||
### synapse-s3-storage-provider
|
||||
|
||||
https://github.com/matrix-org/synapse-s3-storage-provider
|
||||
|
||||
### matrix-media-repo
|
||||
|
||||
https://github.com/turt2live/matrix-media-repo
|
||||
|
||||
## Pixelfed
|
||||
|
||||
https://docs.pixelfed.org/technical-documentation/env.html#filesystem
|
||||
|
||||
## Pleroma
|
||||
|
||||
https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3
|
||||
|
||||
## Lemmy
|
||||
|
||||
via pict-rs
|
||||
https://git.asonix.dog/asonix/pict-rs/commit/f9f4fc63d670f357c93f24147c2ee3e1278e2d97
|
||||
|
||||
## Funkwhale
|
||||
|
||||
https://docs.funkwhale.audio/admin/configuration.html#s3-storage
|
||||
|
||||
## Misskey
|
||||
|
||||
https://github.com/misskey-dev/misskey/commit/9d944243a3a59e8880a360cbfe30fd5a3ec8d52d
|
||||
|
||||
## Prismo
|
||||
|
||||
https://gitlab.com/prismosuite/prismo/-/blob/dev/.env.production.sample#L26-33
|
||||
|
||||
## Owncloud Infinite Scale (ocis)
|
||||
|
||||
## Unsupported
|
||||
|
||||
- Mobilizon: No S3 integration
|
||||
- WriteFreely: No S3 integration
|
||||
- Plume: No S3 integration
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,127 @@
|
||||
# CLI tools
|
||||
|
||||
CLI tools allow you to query the S3 API without too many abstractions.
|
||||
These tools are particularly suitable for debug, backups, website deployments or any scripted task that need to handle data.
|
||||
|
||||
## Minio client (recommended)
|
||||
|
||||
Use the following command to set an "alias", i.e. define a new S3 server to be
|
||||
used by the Minio client:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
garage \
|
||||
<endpoint> \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
Remember that `mc` is sometimes called `mcli` (such as on Arch Linux), to avoid conflicts
|
||||
with Midnight Commander.
|
||||
|
||||
Some commands:
|
||||
|
||||
```bash
|
||||
# list buckets
|
||||
mc ls garage/
|
||||
|
||||
# list objets in a bucket
|
||||
mc ls garage/my_files
|
||||
|
||||
# copy from your filesystem to garage
|
||||
mc cp /proc/cpuinfo garage/my_files/cpuinfo.txt
|
||||
|
||||
# copy from garage to your filesystem
|
||||
mc cp garage/my_files/cpuinfo.txt /tmp/cpuinfo.txt
|
||||
|
||||
# mirror a folder from your filesystem to garage
|
||||
mc mirror --overwrite ./book garage/garagehq.deuxfleurs.fr
|
||||
```
|
||||
|
||||
|
||||
## AWS CLI
|
||||
|
||||
Create a file named `~/.aws/credentials` and put:
|
||||
|
||||
```toml
|
||||
[default]
|
||||
aws_access_key_id=xxxx
|
||||
aws_secret_access_key=xxxx
|
||||
```
|
||||
|
||||
Then a file named `~/.aws/config` and put:
|
||||
|
||||
```toml
|
||||
[default]
|
||||
region=garage
|
||||
```
|
||||
|
||||
Now, supposing Garage is listening on `http://127.0.0.1:3900`, you can list your buckets with:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url http://127.0.0.1:3900 s3 ls
|
||||
```
|
||||
|
||||
Passing the `--endpoint-url` parameter to each command is annoying but AWS developers do not provide a corresponding configuration entry.
|
||||
As a workaround, you can redefine the aws command by editing the file `~/.bashrc`:
|
||||
|
||||
```
|
||||
function aws { command aws --endpoint-url http://127.0.0.1:3900 $@ ; }
|
||||
```
|
||||
|
||||
*Do not forget to run `source ~/.bashrc` or to start a new terminal before running the next commands.*
|
||||
|
||||
Now you can simply run:
|
||||
|
||||
```bash
|
||||
# list buckets
|
||||
aws s3 ls
|
||||
|
||||
# list objects of a bucket
|
||||
aws s3 ls s3://my_files
|
||||
|
||||
# copy from your filesystem to garage
|
||||
aws s3 cp /proc/cpuinfo s3://my_files/cpuinfo.txt
|
||||
|
||||
# copy from garage to your filesystem
|
||||
aws s3 cp s3/my_files/cpuinfo.txt /tmp/cpuinfo.txt
|
||||
```
|
||||
|
||||
## `rclone`
|
||||
|
||||
`rclone` can be configured using the interactive assistant invoked using `rclone configure`.
|
||||
|
||||
You can also configure `rclone` by writing directly its configuration file.
|
||||
Here is a template `rclone.ini` configuration file:
|
||||
|
||||
```ini
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = <access key>
|
||||
secret_access_key = <secret key>
|
||||
region = <region>
|
||||
endpoint = <endpoint>
|
||||
force_path_style = true
|
||||
acl = private
|
||||
bucket_acl = private
|
||||
```
|
||||
|
||||
## Cyberduck
|
||||
|
||||
TODO
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
Here is a template for the `s3cmd.cfg` file to talk with Garage:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
access_key = <access key>
|
||||
secret_key = <secret key>
|
||||
host_base = <endpoint without http(s)://>
|
||||
host_bucket = <same as host_base>
|
||||
use_https = False | True
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# Your code (PHP, JS, Go...)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Configuring S3 clients to interact with Garage
|
||||
# Connect it to
|
||||
|
||||
To configure an S3 client to interact with Garage, you will need the following
|
||||
parameters:
|
||||
@@ -28,78 +28,11 @@ provided that you follow the following guidelines:
|
||||
Garage should normally redirect your client to the correct region,
|
||||
but in case your client does not support this you might have to configure it manually.
|
||||
|
||||
We will now provide example configurations for the most common S3 clients.
|
||||
We will now provide example configurations for the most common clients per category:
|
||||
|
||||
## AWS CLI
|
||||
- [Apps](./apps.md)
|
||||
- [Websites](./websites.md)
|
||||
- [Repositories](./repositories.md)
|
||||
- [CLI tools](./cli.md)
|
||||
- [Your code](./code.md)
|
||||
|
||||
Export the following environment variables:
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID=<access key>
|
||||
export AWS_SECRET_ACCESS_KEY=<secret key>
|
||||
export AWS_DEFAULT_REGION=<region>
|
||||
```
|
||||
|
||||
Now invoke `aws` as follows:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url <endpoint> s3 <command...>
|
||||
```
|
||||
|
||||
For instance: `aws --endpoint-url http://127.0.0.1:3901 s3 ls s3://my-bucket/`.
|
||||
|
||||
## Minio client
|
||||
|
||||
Use the following command to set an "alias", i.e. define a new S3 server to be
|
||||
used by the Minio client:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
garage \
|
||||
<endpoint> \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
Remember that `mc` is sometimes called `mcli` (such as on Arch Linux), to avoid conflicts
|
||||
with the Midnight Commander.
|
||||
|
||||
|
||||
## `rclone`
|
||||
|
||||
`rclone` can be configured using the interactive assistant invoked using `rclone configure`.
|
||||
|
||||
You can also configure `rclone` by writing directly its configuration file.
|
||||
Here is a template `rclone.ini` configuration file:
|
||||
|
||||
```ini
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = <access key>
|
||||
secret_access_key = <secret key>
|
||||
region = <region>
|
||||
endpoint = <endpoint>
|
||||
force_path_style = true
|
||||
acl = private
|
||||
bucket_acl = private
|
||||
```
|
||||
|
||||
## Cyberduck
|
||||
|
||||
TODO
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
Here is a template for the `s3cmd.cfg` file to talk with Garage:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
access_key = <access key>
|
||||
secret_key = <secret key>
|
||||
host_base = <endpoint without http(s)://>
|
||||
host_bucket = <same as host_base>
|
||||
use_https = False | True
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# Repositories (Docker, Nix...)
|
||||
@@ -0,0 +1 @@
|
||||
# Websites (Hugo, Jekyll, Publii...)
|
||||
@@ -0,0 +1 @@
|
||||
# Configuring a reverse proxy
|
||||
@@ -92,10 +92,21 @@ caching our development dependencies.
|
||||
*Currently there is no automatic garbage collection of the cache: we should monitor its growth.
|
||||
Hopefully, we can erase it totally without breaking any build, the next build will only be slower.*
|
||||
|
||||
In practise, we concluded that we do not want to cache all the compilation dependencies.
|
||||
Instead, we want to cache the toolchain we use to build Garage each time we change it.
|
||||
So we removed from Drone any automatic update of the cache and instead handle them manually with:
|
||||
|
||||
```
|
||||
source ~/.awsrc
|
||||
nix-shell --run 'refresh_toolchain'
|
||||
```
|
||||
|
||||
Internally, it will run `nix-build` on `nix/toolchain.nix` and send the output plus its depedencies to the cache.
|
||||
|
||||
To erase the cache:
|
||||
|
||||
```
|
||||
mc rm --recursive --force 'garage/nix/*'
|
||||
mc rm --recursive --force 'garage/nix/'
|
||||
```
|
||||
|
||||
### Publishing Garage
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center" style="text-align:center;">
|
||||
[ <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
|
||||
[ <a href="https://garagehq.deuxfleurs.fr/_releases.html">Download</a>
|
||||
| <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
|
||||
| <a href="https://matrix.to/#/%23garage:deuxfleurs.fr">Matrix channel</a>
|
||||
| <a href="https://drone.deuxfleurs.fr/Deuxfleurs/garage">Drone CI</a>
|
||||
]
|
||||
@@ -114,7 +115,7 @@ Please note that if you patch Garage and then use it to provide any service over
|
||||
|
||||
# Funding
|
||||
|
||||
The Deuxfleurs association has received a grant promise to fund 3 people working on Garage for a year, from October 2021 to September 2022.
|
||||
The Deuxfleurs association has received a grant from [NGI POINTER](https://pointer.ngi.eu/), to fund 3 people working on Garage full-time for a year: from October 2021 to September 2022.
|
||||
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://pointer.ngi.eu/">
|
||||
@@ -125,4 +126,4 @@ The Deuxfleurs association has received a grant promise to fund 3 people working
|
||||
</a>
|
||||
</div>
|
||||
|
||||
_This project has received funding from the European Union’s Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement No 871528._
|
||||
_This project has received funding from the European Union’s Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement N° 871528._
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Migrating from 0.3 to 0.4
|
||||
|
||||
**Migrating from 0.3 to 0.4 is unsupported. This document is only intended to document the process internally for the Deuxfleurs cluster where we have to do it. Do not try it yourself, you will lose your data and we will not help you.**
|
||||
**Migrating from 0.3 to 0.4 is unsupported. This document is only intended to
|
||||
document the process internally for the Deuxfleurs cluster where we have to do
|
||||
it. Do not try it yourself, you will lose your data and we will not help you.**
|
||||
|
||||
**Migrating from 0.2 to 0.4 will break everything for sure. Never try it.**
|
||||
|
||||
@@ -9,53 +11,95 @@ The Sled database is still the same, and the data directory as well.
|
||||
|
||||
The following has changed, all in the meta directory:
|
||||
|
||||
- `node_id` in 0.3 contains the identifier of the current node. In 0.4, this file does nothing and should be deleted. It is replaced by `node_key` (the secret key) and `node_key.pub` (the associated public key). A node's identifier on the ring is its public key.
|
||||
- `node_id` in 0.3 contains the identifier of the current node. In 0.4, this
|
||||
file does nothing and should be deleted. It is replaced by `node_key` (the
|
||||
secret key) and `node_key.pub` (the associated public key). A node's
|
||||
identifier on the ring is its public key.
|
||||
|
||||
- `peer_info` in 0.3 contains the list of peers saved automatically by Garage. The format has changed and it is now stored in `peer_list` (`peer_info` should be deleted).
|
||||
- `peer_info` in 0.3 contains the list of peers saved automatically by Garage.
|
||||
The format has changed and it is now stored in `peer_list` (`peer_info`
|
||||
should be deleted).
|
||||
|
||||
When migrating, all node identifiers will change. This also means that the affectation of data partitions on the ring will change, and lots of data will have to be rebalanced.
|
||||
When migrating, all node identifiers will change. This also means that the
|
||||
affectation of data partitions on the ring will change, and lots of data will
|
||||
have to be rebalanced.
|
||||
|
||||
- If your cluster has only 3 nodes, all nodes store everything, therefore nothing has to be rebalanced.
|
||||
|
||||
- If your cluster has only 4 nodes, for any partition there will always be at least 2 nodes that stored data before that still store it after. Therefore the migration should in theory be transparent and Garage should continue to work during the rebalance.
|
||||
- If your cluster has only 4 nodes, for any partition there will always be at
|
||||
least 2 nodes that stored data before that still store it after. Therefore
|
||||
the migration should in theory be transparent and Garage should continue to
|
||||
work during the rebalance.
|
||||
|
||||
- If your cluster has 5 or more nodes, data will disappear during the migration. Do not migrate (fortunately we don't have this scenario at Deuxfleurs), or if you do, make Garage unavailable until things stabilize (disable web and api access).
|
||||
- If your cluster has 5 or more nodes, data will disappear during the
|
||||
migration. Do not migrate (fortunately we don't have this scenario at
|
||||
Deuxfleurs), or if you do, make Garage unavailable until things stabilize
|
||||
(disable web and api access).
|
||||
|
||||
|
||||
The migration steps are as follows:
|
||||
|
||||
1. Prepare a new configuration file for 0.4. For each node, point to the same meta and data directories as Garage 0.3. Basically, the things that change are the following:
|
||||
1. Prepare a new configuration file for 0.4. For each node, point to the same
|
||||
meta and data directories as Garage 0.3. Basically, the things that change
|
||||
are the following:
|
||||
|
||||
- No more `rpc_tls` section
|
||||
- You have to generate a shared `rpc_secret` and put it in all config files
|
||||
- `bootstrap_nodes` has a different syntax as it has to contain node keys. Leave it empty and use `garage node-id` and `garage node connect` instead (new features of 0.4)
|
||||
- `bootstrap_peers` has a different syntax as it has to contain node keys.
|
||||
Leave it empty and use `garage node-id` and `garage node connect` instead (new features of 0.4)
|
||||
- put the publicly accessible RPC address of your node in `rpc_public_addr` if possible (its optional but recommended)
|
||||
- If you are using Consul, change the `consul_service_name` to NOT be the name advertised by Nomad. Now Garage is responsible for advertising its own service itself.
|
||||
- If you are using Consul, change the `consul_service_name` to NOT be the name advertised by Nomad.
|
||||
Now Garage is responsible for advertising its own service itself.
|
||||
|
||||
2. Disable api and web access for some time, do `garage repair --all --yes tables` and `garage repair --all --yes blocks`, check the logs and check that all data seems to be synced correctly between nodes.
|
||||
2. Disable api and web access for some time (Garage does not support disabling
|
||||
these endpoints but you can change the port number or stop your reverse
|
||||
proxy for instance).
|
||||
|
||||
3. Save somewhere the output of `garage status`. We will need this to remember how to reconfigure nodes in 0.4.
|
||||
3. Do `garage repair -a --yes tables` and `garage repair -a --yes blocks`,
|
||||
check the logs and check that all data seems to be synced correctly between
|
||||
nodes.
|
||||
|
||||
4. Turn off Garage 0.3
|
||||
4. Save somewhere the output of `garage status`. We will need this to remember
|
||||
how to reconfigure nodes in 0.4.
|
||||
|
||||
5. Backup metadata folders if you can (i.e. if you have space to do it somewhere). Backuping data folders could also be usefull but that's much harder to do. If your filesystem supports snapshots, this could be a good time to use them.
|
||||
5. Turn off Garage 0.3
|
||||
|
||||
6. Turn on Garage 0.4
|
||||
6. Backup metadata folders if you can (i.e. if you have space to do it
|
||||
somewhere). Backuping data folders could also be usefull but that's much
|
||||
harder to do. If your filesystem supports snapshots, this could be a good
|
||||
time to use them.
|
||||
|
||||
7. At this point, running `garage status` should indicate that all nodes of the previous cluster are "unavailable". The nodes have new identifiers that should appear in healthy nodes once they can talk to one another (use `garage node connect` if necessary`). They should have NO ROLE ASSIGNED at the moment.
|
||||
7. Turn on Garage 0.4
|
||||
|
||||
8. Prepare a script with several `garage node configure` commands that replace each of the v0.3 node ID with the corresponding v0.4 node ID, with the same zone/tag/capacity. For example if your node `drosera` had identifier `c24e` before and now has identifier `789a`, and it was configured with capacity `2` in zone `dc1`, put the following command in your script:
|
||||
8. At this point, running `garage status` should indicate that all nodes of the
|
||||
previous cluster are "unavailable". The nodes have new identifiers that
|
||||
should appear in healthy nodes once they can talk to one another (use
|
||||
`garage node connect` if necessary`). They should have NO ROLE ASSIGNED at
|
||||
the moment.
|
||||
|
||||
9. Prepare a script with several `garage node configure` commands that replace
|
||||
each of the v0.3 node ID with the corresponding v0.4 node ID, with the same
|
||||
zone/tag/capacity. For example if your node `drosera` had identifier `c24e`
|
||||
before and now has identifier `789a`, and it was configured with capacity
|
||||
`2` in zone `dc1`, put the following command in your script:
|
||||
|
||||
```bash
|
||||
garage node configure 789a -z dc1 -c 2 -t drosera --replace c24e
|
||||
```
|
||||
|
||||
9. Run your reconfiguration script. Check that the new output of `garage status` contains the correct node IDs with the correct values for capacity and zone. Old nodes should no longer be mentioned.
|
||||
10. Run your reconfiguration script. Check that the new output of `garage
|
||||
status` contains the correct node IDs with the correct values for capacity
|
||||
and zone. Old nodes should no longer be mentioned.
|
||||
|
||||
10. If your cluster has 4 nodes or less, and you are feeling adventurous, you can reenable Web and API access now. Things will probably work.
|
||||
11. If your cluster has 4 nodes or less, and you are feeling adventurous, you
|
||||
can reenable Web and API access now. Things will probably work.
|
||||
|
||||
11. Garage might already be resyncing stuff. Issue a `garage repair --all --yes tables` and `garage repair --all --yes blocks` to force it to do so.
|
||||
12. Garage might already be resyncing stuff. Issue a `garage repair -a --yes
|
||||
tables` and `garage repair -a --yes blocks` to force it to do so.
|
||||
|
||||
12. Wait for resyncing activity to stop in the logs. Do steps 11 and 12 two or three times, until you see that when you issue the repair commands, nothing gets resynced any longer.
|
||||
13. Wait for resyncing activity to stop in the logs. Do steps 12 and 13 two or
|
||||
three times, until you see that when you issue the repair commands, nothing
|
||||
gets resynced any longer.
|
||||
|
||||
13. Your upgraded cluster should be in a working state. Re-enable API and Web access and check that everything went well.
|
||||
14. Your upgraded cluster should be in a working state. Re-enable API and Web
|
||||
access and check that everything went well.
|
||||
|
||||
@@ -12,16 +12,10 @@ rec {
|
||||
url = "https://github.com/superboum/cargo2nix";
|
||||
rev = "1364752cd784764db2ef5b1e1248727cebfae2ce";
|
||||
};
|
||||
rustOverlaySrc = fetchGit {
|
||||
# As of 2021-10-05
|
||||
url = "https://github.com/oxalica/rust-overlay";
|
||||
rev = "9c2fc6a62ccbc6f420d71ecac6bf0b84dbbee64f";
|
||||
};
|
||||
|
||||
/*
|
||||
* Shared objects
|
||||
*/
|
||||
rustOverlay = import rustOverlaySrc;
|
||||
cargo2nix = import cargo2nixSrc;
|
||||
cargo2nixOverlay = import "${cargo2nixSrc}/overlay";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,3 @@ substituters = https://cache.nixos.org https://nix.web.deuxfleurs.fr
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix.web.deuxfleurs.fr:eTGL6kvaQn6cDR/F9lDYUIP9nCVR/kkshYfLDJf1yKs=
|
||||
max-jobs = auto
|
||||
cores = 4
|
||||
|
||||
# required for containers
|
||||
sandbox = false
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
}:
|
||||
|
||||
with import ./common.nix;
|
||||
|
||||
let
|
||||
platforms = [
|
||||
"x86_64-unknown-linux-musl"
|
||||
"i686-unknown-linux-musl"
|
||||
"aarch64-unknown-linux-musl"
|
||||
"armv6l-unknown-linux-musleabihf"
|
||||
];
|
||||
pkgsList = builtins.map (target: import pkgsSrc {
|
||||
inherit system;
|
||||
crossSystem = { config = target; };
|
||||
}) platforms;
|
||||
pkgsHost = import pkgsSrc {};
|
||||
lib = pkgsHost.lib;
|
||||
kaniko = (import ./kaniko.nix) pkgsHost;
|
||||
in
|
||||
lib.flatten (builtins.map (pkgs: [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.buildPackages.stdenv.cc
|
||||
]) pkgsList) ++ [
|
||||
kaniko
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
crossSystem ? null,
|
||||
rust ? true,
|
||||
integration ? true,
|
||||
release ? true,
|
||||
@@ -10,10 +9,9 @@ with import ./nix/common.nix;
|
||||
|
||||
let
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay rustOverlay ];
|
||||
inherit system;
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
rustDist = pkgs.buildPackages.rust-bin.stable.latest.default;
|
||||
kaniko = (import ./nix/kaniko.nix) pkgs;
|
||||
|
||||
in
|
||||
@@ -57,13 +55,39 @@ function refresh_index {
|
||||
result \
|
||||
s3://garagehq.deuxfleurs.fr/_releases.html
|
||||
}
|
||||
|
||||
function refresh_toolchain {
|
||||
nix copy \
|
||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR \
|
||||
$(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix))
|
||||
}
|
||||
'';
|
||||
|
||||
nativeBuildInputs =
|
||||
(if rust then [ rustDist (pkgs.callPackage cargo2nix {}).package ] else [])
|
||||
(if rust then [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
/*(pkgs.callPackage cargo2nix {}).package*/
|
||||
] else [])
|
||||
++
|
||||
(if integration then [ pkgs.s3cmd pkgs.awscli2 pkgs.minio-client pkgs.rclone pkgs.socat pkgs.psmisc pkgs.which ] else [])
|
||||
(if integration then [
|
||||
pkgs.s3cmd
|
||||
pkgs.awscli2
|
||||
pkgs.minio-client
|
||||
pkgs.rclone
|
||||
pkgs.socat
|
||||
pkgs.psmisc
|
||||
pkgs.which
|
||||
pkgs.openssl
|
||||
pkgs.curl
|
||||
] else [])
|
||||
++
|
||||
(if release then [ pkgs.awscli2 kaniko ] else [])
|
||||
(if release then [
|
||||
pkgs.awscli2
|
||||
kaniko
|
||||
] else [])
|
||||
;
|
||||
}
|
||||
|
||||
+16
-19
@@ -39,7 +39,7 @@ pub async fn run_api_server(
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let server = Server::bind(addr).serve(service);
|
||||
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
||||
info!("API server listening on http://{}", addr);
|
||||
@@ -88,8 +88,8 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
|
||||
let (bucket, key) = parse_bucket_key(&path)?;
|
||||
let allowed = match req.method() {
|
||||
&Method::HEAD | &Method::GET => api_key.allow_read(&bucket),
|
||||
_ => api_key.allow_write(&bucket),
|
||||
&Method::HEAD | &Method::GET => api_key.allow_read(bucket),
|
||||
_ => api_key.allow_write(bucket),
|
||||
};
|
||||
if !allowed {
|
||||
return Err(Error::Forbidden(
|
||||
@@ -109,11 +109,11 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
match *req.method() {
|
||||
Method::HEAD => {
|
||||
// HeadObject query
|
||||
Ok(handle_head(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_head(garage, &req, bucket, key).await?)
|
||||
}
|
||||
Method::GET => {
|
||||
// GetObject query
|
||||
Ok(handle_get(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_get(garage, &req, bucket, key).await?)
|
||||
}
|
||||
Method::PUT => {
|
||||
if params.contains_key(&"partnumber".to_string())
|
||||
@@ -125,8 +125,8 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
Ok(handle_put_part(
|
||||
garage,
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
bucket,
|
||||
key,
|
||||
part_number,
|
||||
upload_id,
|
||||
content_sha256,
|
||||
@@ -136,46 +136,43 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
// CopyObject query
|
||||
let copy_source = req.headers().get("x-amz-copy-source").unwrap().to_str()?;
|
||||
let copy_source =
|
||||
percent_encoding::percent_decode_str(©_source).decode_utf8()?;
|
||||
percent_encoding::percent_decode_str(copy_source).decode_utf8()?;
|
||||
let (source_bucket, source_key) = parse_bucket_key(©_source)?;
|
||||
if !api_key.allow_read(&source_bucket) {
|
||||
if !api_key.allow_read(source_bucket) {
|
||||
return Err(Error::Forbidden(format!(
|
||||
"Reading from bucket {} not allowed for this key",
|
||||
source_bucket
|
||||
)));
|
||||
}
|
||||
let source_key = source_key.ok_or_bad_request("No source key specified")?;
|
||||
Ok(
|
||||
handle_copy(garage, &req, &bucket, &key, &source_bucket, &source_key)
|
||||
.await?,
|
||||
)
|
||||
Ok(handle_copy(garage, &req, bucket, key, source_bucket, source_key).await?)
|
||||
} else {
|
||||
// PutObject query
|
||||
Ok(handle_put(garage, req, &bucket, &key, content_sha256).await?)
|
||||
Ok(handle_put(garage, req, bucket, key, content_sha256).await?)
|
||||
}
|
||||
}
|
||||
Method::DELETE => {
|
||||
if params.contains_key(&"uploadid".to_string()) {
|
||||
// AbortMultipartUpload query
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
Ok(handle_abort_multipart_upload(garage, &bucket, &key, upload_id).await?)
|
||||
Ok(handle_abort_multipart_upload(garage, bucket, key, upload_id).await?)
|
||||
} else {
|
||||
// DeleteObject query
|
||||
Ok(handle_delete(garage, &bucket, &key).await?)
|
||||
Ok(handle_delete(garage, bucket, key).await?)
|
||||
}
|
||||
}
|
||||
Method::POST => {
|
||||
if params.contains_key(&"uploads".to_string()) {
|
||||
// CreateMultipartUpload call
|
||||
Ok(handle_create_multipart_upload(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_create_multipart_upload(garage, &req, bucket, key).await?)
|
||||
} else if params.contains_key(&"uploadid".to_string()) {
|
||||
// CompleteMultipartUpload call
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
Ok(handle_complete_multipart_upload(
|
||||
garage,
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
bucket,
|
||||
key,
|
||||
upload_id,
|
||||
content_sha256,
|
||||
)
|
||||
|
||||
@@ -55,7 +55,8 @@ async fn handle_delete_internal(
|
||||
);
|
||||
|
||||
garage.object_table.insert(&object).await?;
|
||||
return Ok((deleted_version, version_uuid));
|
||||
|
||||
Ok((deleted_version, version_uuid))
|
||||
}
|
||||
|
||||
pub async fn handle_delete(
|
||||
@@ -82,7 +83,7 @@ pub async fn handle_delete_objects(
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let cmd_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
||||
let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
|
||||
|
||||
let mut ret_deleted = Vec::new();
|
||||
|
||||
+5
-5
@@ -106,12 +106,12 @@ pub async fn handle_head(
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(cached) = try_answer_cached(&version, version_meta, req) {
|
||||
if let Some(cached) = try_answer_cached(version, version_meta, req) {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let body: Body = Body::empty();
|
||||
let response = object_headers(&version, version_meta)
|
||||
let response = object_headers(version, version_meta)
|
||||
.header("Content-Length", format!("{}", version_meta.size))
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
@@ -149,7 +149,7 @@ pub async fn handle_get(
|
||||
ObjectVersionData::FirstBlock(meta, _) => meta,
|
||||
};
|
||||
|
||||
if let Some(cached) = try_answer_cached(&last_v, last_v_meta, req) {
|
||||
if let Some(cached) = try_answer_cached(last_v, last_v_meta, req) {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ pub async fn handle_get(
|
||||
.await;
|
||||
}
|
||||
|
||||
let resp_builder = object_headers(&last_v, last_v_meta)
|
||||
let resp_builder = object_headers(last_v, last_v_meta)
|
||||
.header("Content-Length", format!("{}", last_v_meta.size))
|
||||
.status(StatusCode::OK);
|
||||
|
||||
@@ -190,7 +190,7 @@ pub async fn handle_get(
|
||||
Ok(resp_builder.body(body)?)
|
||||
}
|
||||
ObjectVersionData::FirstBlock(_, first_block_hash) => {
|
||||
let read_first_block = garage.block_manager.rpc_get_block(&first_block_hash);
|
||||
let read_first_block = garage.block_manager.rpc_get_block(first_block_hash);
|
||||
let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey);
|
||||
|
||||
let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?;
|
||||
|
||||
+5
-5
@@ -193,8 +193,8 @@ async fn read_and_put_blocks(
|
||||
|
||||
let mut next_offset = first_block.len();
|
||||
let mut put_curr_version_block = put_block_meta(
|
||||
&garage,
|
||||
&version,
|
||||
garage,
|
||||
version,
|
||||
part_number,
|
||||
0,
|
||||
first_block_hash,
|
||||
@@ -213,8 +213,8 @@ async fn read_and_put_blocks(
|
||||
let block_hash = blake2sum(&block[..]);
|
||||
let block_len = block.len();
|
||||
put_curr_version_block = put_block_meta(
|
||||
&garage,
|
||||
&version,
|
||||
garage,
|
||||
version,
|
||||
part_number,
|
||||
next_offset as u64,
|
||||
block_hash,
|
||||
@@ -437,7 +437,7 @@ pub async fn handle_complete_multipart_upload(
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let body_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
||||
let body_list_of_parts = parse_complete_multpart_upload_body(&body_xml)
|
||||
.ok_or_bad_request("Invalid CompleteMultipartUpload XML")?;
|
||||
debug!(
|
||||
|
||||
@@ -70,7 +70,7 @@ pub async fn check_signature(
|
||||
let canonical_request = canonical_request(
|
||||
request.method(),
|
||||
&request.uri().path().to_string(),
|
||||
&canonical_query_string(&request.uri()),
|
||||
&canonical_query_string(request.uri()),
|
||||
&headers,
|
||||
&authorization.signed_headers,
|
||||
&authorization.content_sha256,
|
||||
@@ -252,7 +252,7 @@ fn canonical_request(
|
||||
method.as_str(),
|
||||
url_path,
|
||||
canonical_query_string,
|
||||
&canonical_header_string(&headers, signed_headers),
|
||||
&canonical_header_string(headers, signed_headers),
|
||||
"",
|
||||
signed_headers,
|
||||
content_sha256,
|
||||
|
||||
@@ -41,4 +41,5 @@ futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
pub const READ_KEY_ERROR: &str = "Unable to read node key. It will be generated by your garage node the first time is it launched. Ensure that your garage node is currently running. (The node key is supposed to be stored in your metadata directory.)";
|
||||
@@ -22,11 +24,11 @@ pub fn node_id_command(config_file: PathBuf, quiet: bool) -> Result<(), Error> {
|
||||
println!("{}", idstr);
|
||||
|
||||
if !quiet {
|
||||
eprintln!("WARNING: I don't know the public address to reach this node.");
|
||||
eprintln!("In all of the instructions below, replace 127.0.0.1:3901 by the appropriate address and port.");
|
||||
warn!("WARNING: I don't know the public address to reach this node.");
|
||||
warn!("In all of the instructions below, replace 127.0.0.1:{} by the appropriate address and port.", config.rpc_bind_addr.port());
|
||||
}
|
||||
|
||||
format!("{}@127.0.0.1:3901", idstr)
|
||||
format!("{}@127.0.0.1:{}", idstr, config.rpc_bind_addr.port())
|
||||
};
|
||||
|
||||
if !quiet {
|
||||
|
||||
@@ -265,7 +265,7 @@ pub struct RepairOpt {
|
||||
pub yes: bool,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
pub what: Option<RepairWhat>,
|
||||
pub what: RepairWhat,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
@@ -282,6 +282,13 @@ pub enum RepairWhat {
|
||||
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
|
||||
#[structopt(name = "block_refs")]
|
||||
BlockRefs,
|
||||
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
|
||||
#[structopt(name = "scrub")]
|
||||
Scrub {
|
||||
/// Tranquility factor (see tranquilizer documentation)
|
||||
#[structopt(name = "tranquility", default_value = "2")]
|
||||
tranquility: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)]
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn format_table(data: Vec<String>) {
|
||||
out.push_str(col);
|
||||
(0..col_len - col.chars().count() + 2).for_each(|_| out.push(' '));
|
||||
}
|
||||
out.push_str(&row[row.len() - 1]);
|
||||
out.push_str(row[row.len() - 1]);
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
|
||||
+28
-8
@@ -9,6 +9,7 @@ mod cli;
|
||||
mod repair;
|
||||
mod server;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use structopt::StructOpt;
|
||||
@@ -29,15 +30,20 @@ use cli::*;
|
||||
struct Opt {
|
||||
/// Host to connect to for admin operations, in the format:
|
||||
/// <public-key>@<ip>:<port>
|
||||
#[structopt(short = "h", long = "rpc-host")]
|
||||
#[structopt(short = "h", long = "rpc-host", env = "GARAGE_RPC_HOST")]
|
||||
pub rpc_host: Option<String>,
|
||||
|
||||
/// RPC secret network key for admin operations
|
||||
#[structopt(short = "s", long = "rpc-secret")]
|
||||
#[structopt(short = "s", long = "rpc-secret", env = "GARAGE_RPC_SECRET")]
|
||||
pub rpc_secret: Option<String>,
|
||||
|
||||
/// Configuration file (garage.toml)
|
||||
#[structopt(short = "c", long = "config", default_value = "/etc/garage.toml")]
|
||||
#[structopt(
|
||||
short = "c",
|
||||
long = "config",
|
||||
env = "GARAGE_CONFIG_FILE",
|
||||
default_value = "/etc/garage.toml"
|
||||
)]
|
||||
pub config_file: PathBuf,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
@@ -46,6 +52,9 @@ struct Opt {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "garage=info")
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
sodiumoxide::init().expect("Unable to init sodiumoxide");
|
||||
|
||||
@@ -99,12 +108,23 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
|
||||
let (id, addr) = if let Some(h) = opt.rpc_host {
|
||||
let (id, addrs) = parse_and_resolve_peer_addr(&h).ok_or_else(|| format!("Invalid RPC remote node identifier: {}. Expected format is <pubkey>@<IP or hostname>:<port>.", h))?;
|
||||
(id, addrs[0])
|
||||
} else if let Some(a) = config.as_ref().map(|c| c.rpc_public_addr).flatten() {
|
||||
let node_id = garage_rpc::system::read_node_id(&config.unwrap().metadata_dir)
|
||||
.err_context(READ_KEY_ERROR)?;
|
||||
(node_id, a)
|
||||
} else {
|
||||
return Err(Error::Message("No RPC host provided".into()));
|
||||
let node_id = garage_rpc::system::read_node_id(&config.as_ref().unwrap().metadata_dir)
|
||||
.err_context(READ_KEY_ERROR)?;
|
||||
if let Some(a) = config.as_ref().map(|c| c.rpc_public_addr).flatten() {
|
||||
(node_id, a)
|
||||
} else {
|
||||
let default_addr = SocketAddr::new(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
config.as_ref().unwrap().rpc_bind_addr.port(),
|
||||
);
|
||||
warn!(
|
||||
"Trying to contact Garage node at default address {}",
|
||||
default_addr
|
||||
);
|
||||
warn!("If this doesn't work, consider adding rpc_public_addr in your config file or specifying the -h command line parameter.");
|
||||
(node_id, default_addr)
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to target host
|
||||
|
||||
+31
-41
@@ -27,42 +27,38 @@ impl Repair {
|
||||
opt: RepairOpt,
|
||||
must_exit: watch::Receiver<bool>,
|
||||
) -> Result<(), Error> {
|
||||
let todo = |x| opt.what.as_ref().map(|y| *y == x).unwrap_or(true);
|
||||
|
||||
if todo(RepairWhat::Tables) {
|
||||
info!("Launching a full sync of tables");
|
||||
self.garage.bucket_table.syncer.add_full_sync();
|
||||
self.garage.object_table.syncer.add_full_sync();
|
||||
self.garage.version_table.syncer.add_full_sync();
|
||||
self.garage.block_ref_table.syncer.add_full_sync();
|
||||
self.garage.key_table.syncer.add_full_sync();
|
||||
match opt.what {
|
||||
RepairWhat::Tables => {
|
||||
info!("Launching a full sync of tables");
|
||||
self.garage.bucket_table.syncer.add_full_sync();
|
||||
self.garage.object_table.syncer.add_full_sync();
|
||||
self.garage.version_table.syncer.add_full_sync();
|
||||
self.garage.block_ref_table.syncer.add_full_sync();
|
||||
self.garage.key_table.syncer.add_full_sync();
|
||||
}
|
||||
RepairWhat::Versions => {
|
||||
info!("Repairing the versions table");
|
||||
self.repair_versions(&must_exit).await?;
|
||||
}
|
||||
RepairWhat::BlockRefs => {
|
||||
info!("Repairing the block refs table");
|
||||
self.repair_block_ref(&must_exit).await?;
|
||||
}
|
||||
RepairWhat::Blocks => {
|
||||
info!("Repairing the stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.repair_data_store(&must_exit)
|
||||
.await?;
|
||||
}
|
||||
RepairWhat::Scrub { tranquility } => {
|
||||
info!("Verifying integrity of stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.scrub_data_store(&must_exit, tranquility)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: wait for full sync to finish before proceeding to the rest?
|
||||
|
||||
if todo(RepairWhat::Versions) {
|
||||
info!("Repairing the versions table");
|
||||
self.repair_versions(&must_exit).await?;
|
||||
}
|
||||
|
||||
if todo(RepairWhat::BlockRefs) {
|
||||
info!("Repairing the block refs table");
|
||||
self.repair_block_ref(&must_exit).await?;
|
||||
}
|
||||
|
||||
if opt.what.is_none() {
|
||||
info!("Repairing the RC");
|
||||
self.repair_rc(&must_exit).await?;
|
||||
}
|
||||
|
||||
if todo(RepairWhat::Blocks) {
|
||||
info!("Repairing the stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.repair_data_store(&must_exit)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -150,10 +146,4 @@ impl Repair {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn repair_rc(&self, _must_exit: &watch::Receiver<bool>) -> Result<(), Error> {
|
||||
// TODO
|
||||
warn!("repair_rc: not implemented");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
+110
-51
@@ -12,8 +12,9 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{watch, Mutex, Notify};
|
||||
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error;
|
||||
use garage_util::error::*;
|
||||
use garage_util::time::*;
|
||||
use garage_util::tranquilizer::Tranquilizer;
|
||||
|
||||
use garage_rpc::system::System;
|
||||
use garage_rpc::*;
|
||||
@@ -27,7 +28,8 @@ use crate::garage::Garage;
|
||||
/// Size under which data will be stored inlined in database instead of as files
|
||||
pub const INLINE_THRESHOLD: usize = 3072;
|
||||
|
||||
pub const BACKGROUND_WORKERS: u64 = 2;
|
||||
pub const BACKGROUND_WORKERS: u64 = 1;
|
||||
pub const BACKGROUND_TRANQUILITY: u32 = 3;
|
||||
|
||||
const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(42);
|
||||
const BLOCK_GC_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
@@ -129,7 +131,7 @@ impl BlockManager {
|
||||
|
||||
/// Ask nodes that might have a block for it
|
||||
pub async fn rpc_get_block(&self, hash: &Hash) -> Result<Vec<u8>, Error> {
|
||||
let who = self.replication.read_nodes(&hash);
|
||||
let who = self.replication.read_nodes(hash);
|
||||
let resps = self
|
||||
.system
|
||||
.rpc
|
||||
@@ -197,10 +199,35 @@ impl BlockManager {
|
||||
}
|
||||
|
||||
// 2. Repair blocks actually on disk
|
||||
self.repair_aux_read_dir_rec(&self.data_dir, must_exit)
|
||||
.await?;
|
||||
// Lists all blocks on disk and adds them to the resync queue.
|
||||
// This allows us to find blocks we are storing but don't actually need,
|
||||
// so that we can offload them if necessary and then delete them locally.
|
||||
self.for_each_file(
|
||||
(),
|
||||
move |_, hash| async move { self.put_to_resync(&hash, Duration::from_secs(0)) },
|
||||
must_exit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Verify integrity of each block on disk. Use `speed_limit` to limit the load generated by
|
||||
/// this function.
|
||||
pub async fn scrub_data_store(
|
||||
&self,
|
||||
must_exit: &watch::Receiver<bool>,
|
||||
tranquility: u32,
|
||||
) -> Result<(), Error> {
|
||||
let tranquilizer = Tranquilizer::new(30);
|
||||
self.for_each_file(
|
||||
tranquilizer,
|
||||
move |mut tranquilizer, hash| async move {
|
||||
let _ = self.read_block(&hash).await;
|
||||
tranquilizer.tranquilize(tranquility).await;
|
||||
Ok(tranquilizer)
|
||||
},
|
||||
must_exit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get lenght of resync queue
|
||||
@@ -224,7 +251,7 @@ impl BlockManager {
|
||||
})?;
|
||||
let old_rc = old_rc.map(u64_from_be_bytes).unwrap_or(0);
|
||||
if old_rc == 0 {
|
||||
self.put_to_resync(&hash, BLOCK_RW_TIMEOUT)?;
|
||||
self.put_to_resync(hash, BLOCK_RW_TIMEOUT)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -240,7 +267,7 @@ impl BlockManager {
|
||||
}
|
||||
})?;
|
||||
if new_rc.is_none() {
|
||||
self.put_to_resync(&hash, BLOCK_GC_TIMEOUT)?;
|
||||
self.put_to_resync(hash, BLOCK_GC_TIMEOUT)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -346,20 +373,33 @@ impl BlockManager {
|
||||
}
|
||||
|
||||
async fn resync_loop(self: Arc<Self>, mut must_exit: watch::Receiver<bool>) {
|
||||
let mut tranquilizer = Tranquilizer::new(30);
|
||||
|
||||
while !*must_exit.borrow() {
|
||||
if let Err(e) = self.resync_iter(&mut must_exit).await {
|
||||
warn!("Error in block resync loop: {}", e);
|
||||
select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(1)).fuse() => {},
|
||||
_ = must_exit.changed().fuse() => {},
|
||||
match self.resync_iter(&mut must_exit).await {
|
||||
Ok(true) => {
|
||||
tranquilizer.tranquilize(BACKGROUND_TRANQUILITY).await;
|
||||
}
|
||||
Ok(false) => {
|
||||
tranquilizer.reset();
|
||||
}
|
||||
Err(e) => {
|
||||
// The errors that we have here are only Sled errors
|
||||
// We don't really know how to handle them so just ¯\_(ツ)_/¯
|
||||
// (there is kind of an assumption that Sled won't error on us,
|
||||
// if it does there is not much we can do -- TODO should we just panic?)
|
||||
error!(
|
||||
"Could not do a resync iteration: {} (this is a very bad error)",
|
||||
e
|
||||
);
|
||||
tranquilizer.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resync_iter(&self, must_exit: &mut watch::Receiver<bool>) -> Result<(), Error> {
|
||||
if let Some(first_item) = self.resync_queue.iter().next() {
|
||||
let (time_bytes, hash_bytes) = first_item?;
|
||||
async fn resync_iter(&self, must_exit: &mut watch::Receiver<bool>) -> Result<bool, Error> {
|
||||
if let Some((time_bytes, hash_bytes)) = self.resync_queue.pop_min()? {
|
||||
let time_msec = u64_from_be_bytes(&time_bytes[0..8]);
|
||||
let now = now_msec();
|
||||
if now >= time_msec {
|
||||
@@ -369,23 +409,24 @@ impl BlockManager {
|
||||
warn!("Error when resyncing {:?}: {}", hash, e);
|
||||
self.put_to_resync(&hash, RESYNC_RETRY_TIMEOUT)?;
|
||||
}
|
||||
self.resync_queue.remove(&time_bytes)?;
|
||||
res?; // propagate error to delay main loop
|
||||
Ok(true)
|
||||
} else {
|
||||
self.resync_queue.insert(time_bytes, hash_bytes)?;
|
||||
let delay = tokio::time::sleep(Duration::from_millis(time_msec - now));
|
||||
select! {
|
||||
_ = delay.fuse() => {},
|
||||
_ = self.resync_notify.notified().fuse() => {},
|
||||
_ = must_exit.changed().fuse() => {},
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
_ = self.resync_notify.notified().fuse() => {},
|
||||
_ = must_exit.changed().fuse() => {},
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resync_block(&self, hash: &Hash) -> Result<(), Error> {
|
||||
@@ -406,7 +447,7 @@ impl BlockManager {
|
||||
if exists && !needed {
|
||||
trace!("Offloading block {:?}", hash);
|
||||
|
||||
let mut who = self.replication.write_nodes(&hash);
|
||||
let mut who = self.replication.write_nodes(hash);
|
||||
if who.len() < self.replication.write_quorum() {
|
||||
return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string()));
|
||||
}
|
||||
@@ -418,7 +459,7 @@ impl BlockManager {
|
||||
&self.endpoint,
|
||||
*to,
|
||||
msg.clone(),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_timeout(NEED_BLOCK_QUERY_TIMEOUT),
|
||||
)
|
||||
});
|
||||
@@ -426,7 +467,7 @@ impl BlockManager {
|
||||
|
||||
let mut need_nodes = vec![];
|
||||
for (node, needed) in who.iter().zip(who_needs_resps.into_iter()) {
|
||||
match needed? {
|
||||
match needed.err_context("NeedBlockQuery RPC")? {
|
||||
BlockRpc::NeedBlockReply(needed) => {
|
||||
if needed {
|
||||
need_nodes.push(*node);
|
||||
@@ -447,14 +488,14 @@ impl BlockManager {
|
||||
need_nodes.len()
|
||||
);
|
||||
|
||||
let put_block_message = self.read_block(hash).await?;
|
||||
let put_block_message = self.read_block(hash).await.err_context("PutBlock RPC")?;
|
||||
self.system
|
||||
.rpc
|
||||
.try_call_many(
|
||||
&self.endpoint,
|
||||
&need_nodes[..],
|
||||
put_block_message,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(need_nodes.len())
|
||||
.with_timeout(BLOCK_RW_TIMEOUT),
|
||||
)
|
||||
@@ -478,54 +519,72 @@ impl BlockManager {
|
||||
// TODO find a way to not do this if they are sending it to us
|
||||
// Let's suppose this isn't an issue for now with the BLOCK_RW_TIMEOUT delay
|
||||
// between the RC being incremented and this part being called.
|
||||
let block_data = self.rpc_get_block(&hash).await?;
|
||||
let block_data = self.rpc_get_block(hash).await?;
|
||||
self.write_block(hash, &block_data[..]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repair_aux_read_dir_rec<'a>(
|
||||
async fn for_each_file<F, Fut, State>(
|
||||
&self,
|
||||
state: State,
|
||||
mut f: F,
|
||||
must_exit: &watch::Receiver<bool>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
F: FnMut(State, Hash) -> Fut + Send,
|
||||
Fut: Future<Output = Result<State, Error>> + Send,
|
||||
State: Send,
|
||||
{
|
||||
self.for_each_file_rec(&self.data_dir, state, &mut f, must_exit)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn for_each_file_rec<'a, F, Fut, State>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
mut state: State,
|
||||
f: &'a mut F,
|
||||
must_exit: &'a watch::Receiver<bool>,
|
||||
) -> BoxFuture<'a, Result<(), Error>> {
|
||||
// Lists all blocks on disk and adds them to the resync queue.
|
||||
// This allows us to find blocks we are storing but don't actually need,
|
||||
// so that we can offload them if necessary and then delete them locally.
|
||||
) -> BoxFuture<'a, Result<State, Error>>
|
||||
where
|
||||
F: FnMut(State, Hash) -> Fut + Send,
|
||||
Fut: Future<Output = Result<State, Error>> + Send,
|
||||
State: Send + 'a,
|
||||
{
|
||||
async move {
|
||||
let mut ls_data_dir = fs::read_dir(path).await?;
|
||||
loop {
|
||||
let data_dir_ent = ls_data_dir.next_entry().await?;
|
||||
let data_dir_ent = match data_dir_ent {
|
||||
Some(x) => x,
|
||||
None => break,
|
||||
};
|
||||
while let Some(data_dir_ent) = ls_data_dir.next_entry().await? {
|
||||
if *must_exit.borrow() {
|
||||
break;
|
||||
}
|
||||
|
||||
let name = data_dir_ent.file_name();
|
||||
let name = match name.into_string() {
|
||||
Ok(x) => x,
|
||||
Err(_) => continue,
|
||||
let name = if let Ok(n) = name.into_string() {
|
||||
n
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let ent_type = data_dir_ent.file_type().await?;
|
||||
|
||||
if name.len() == 2 && hex::decode(&name).is_ok() && ent_type.is_dir() {
|
||||
self.repair_aux_read_dir_rec(&data_dir_ent.path(), must_exit)
|
||||
state = self
|
||||
.for_each_file_rec(&data_dir_ent.path(), state, f, must_exit)
|
||||
.await?;
|
||||
} else if name.len() == 64 {
|
||||
let hash_bytes = match hex::decode(&name) {
|
||||
Ok(h) => h,
|
||||
Err(_) => continue,
|
||||
let hash_bytes = if let Ok(h) = hex::decode(&name) {
|
||||
h
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut hash = [0u8; 32];
|
||||
hash.copy_from_slice(&hash_bytes[..]);
|
||||
self.put_to_resync(&hash.into(), Duration::from_secs(0))?;
|
||||
}
|
||||
|
||||
if *must_exit.borrow() {
|
||||
break;
|
||||
state = f(state, hash.into()).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(state)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
@@ -598,9 +657,9 @@ impl BlockManagerLocked {
|
||||
);
|
||||
let path = mgr.block_path(hash);
|
||||
let mut path2 = path.clone();
|
||||
path2.set_extension(".corrupted");
|
||||
path2.set_extension("corrupted");
|
||||
fs::rename(path, path2).await?;
|
||||
mgr.put_to_resync(&hash, Duration::from_millis(0))?;
|
||||
mgr.put_to_resync(hash, Duration::from_millis(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] }
|
||||
|
||||
|
||||
+179
-45
@@ -7,6 +7,7 @@ use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use futures::stream::StreamExt;
|
||||
use futures_util::future::FutureExt;
|
||||
use tokio::select;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
|
||||
pub use netapp::endpoint::{Endpoint, EndpointHandler, Message as Rpc};
|
||||
use netapp::peering::fullmesh::FullMeshPeeringStrategy;
|
||||
@@ -14,11 +15,18 @@ pub use netapp::proto::*;
|
||||
pub use netapp::{NetApp, NodeID};
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::Uuid;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error;
|
||||
|
||||
use crate::ring::Ring;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
// Try to never have more than 200MB of outgoing requests
|
||||
// buffered at the same time. Other requests are queued until
|
||||
// space is freed.
|
||||
const REQUEST_BUFFER_SIZE: usize = 200 * 1024 * 1024;
|
||||
|
||||
/// Strategy to apply when making RPC
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RequestStrategy {
|
||||
@@ -61,12 +69,32 @@ impl RequestStrategy {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RpcHelper {
|
||||
pub(crate) fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
pub(crate) background: Arc<BackgroundRunner>,
|
||||
pub struct RpcHelper(Arc<RpcHelperInner>);
|
||||
|
||||
struct RpcHelperInner {
|
||||
our_node_id: Uuid,
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
request_buffer_semaphore: Semaphore,
|
||||
}
|
||||
|
||||
impl RpcHelper {
|
||||
pub(crate) fn new(
|
||||
our_node_id: Uuid,
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
) -> Self {
|
||||
Self(Arc::new(RpcHelperInner {
|
||||
our_node_id,
|
||||
fullmesh,
|
||||
background,
|
||||
ring,
|
||||
request_buffer_semaphore: Semaphore::new(REQUEST_BUFFER_SIZE),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn call<M, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
@@ -92,10 +120,23 @@ impl RpcHelper {
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
{
|
||||
let msg_size = rmp_to_vec_all_named(&msg)?.len() as u32;
|
||||
let permit = self
|
||||
.0
|
||||
.request_buffer_semaphore
|
||||
.acquire_many(msg_size)
|
||||
.await?;
|
||||
|
||||
let node_id = to.into();
|
||||
select! {
|
||||
res = endpoint.call(&node_id, &msg, strat.rs_priority) => Ok(res??),
|
||||
_ = tokio::time::sleep(strat.rs_timeout) => Err(Error::Timeout),
|
||||
res = endpoint.call(&node_id, &msg, strat.rs_priority) => {
|
||||
drop(permit);
|
||||
Ok(res??)
|
||||
}
|
||||
_ = tokio::time::sleep(strat.rs_timeout) => {
|
||||
drop(permit);
|
||||
Err(Error::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +174,7 @@ impl RpcHelper {
|
||||
H: EndpointHandler<M>,
|
||||
{
|
||||
let to = self
|
||||
.0
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
@@ -141,8 +183,8 @@ impl RpcHelper {
|
||||
self.call_many(endpoint, &to[..], msg, strat).await
|
||||
}
|
||||
|
||||
/// Make a RPC call to multiple servers, returning either a Vec of responses, or an error if
|
||||
/// strategy could not be respected due to too many errors
|
||||
/// Make a RPC call to multiple servers, returning either a Vec of responses,
|
||||
/// or an error if quorum could not be reached due to too many errors
|
||||
pub async fn try_call_many<M, H, S>(
|
||||
&self,
|
||||
endpoint: &Arc<Endpoint<M, H>>,
|
||||
@@ -156,54 +198,146 @@ impl RpcHelper {
|
||||
S: Send,
|
||||
{
|
||||
let msg = Arc::new(msg);
|
||||
let mut resp_stream = to
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.map(|to| {
|
||||
let self2 = self.clone();
|
||||
let msg = msg.clone();
|
||||
let endpoint2 = endpoint.clone();
|
||||
async move { self2.call_arc(&endpoint2, to, msg, strategy).await }
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let mut results = vec![];
|
||||
let mut errors = vec![];
|
||||
// Build future for each request
|
||||
// They are not started now: they are added below in a FuturesUnordered
|
||||
// object that will take care of polling them (see below)
|
||||
let requests = to.iter().cloned().map(|to| {
|
||||
let self2 = self.clone();
|
||||
let msg = msg.clone();
|
||||
let endpoint2 = endpoint.clone();
|
||||
(to, async move {
|
||||
self2.call_arc(&endpoint2, to, msg, strategy).await
|
||||
})
|
||||
});
|
||||
let quorum = strategy.rs_quorum.unwrap_or(to.len());
|
||||
|
||||
while let Some(resp) = resp_stream.next().await {
|
||||
match resp {
|
||||
Ok(msg) => {
|
||||
results.push(msg);
|
||||
if results.len() >= quorum {
|
||||
break;
|
||||
// Vectors in which success results and errors will be collected
|
||||
let mut successes = vec![];
|
||||
let mut errors = vec![];
|
||||
|
||||
if strategy.rs_interrupt_after_quorum {
|
||||
// Case 1: once quorum is reached, other requests don't matter.
|
||||
// What we do here is only send the required number of requests
|
||||
// to reach a quorum, priorizing nodes with the lowest latency.
|
||||
// When there are errors, we start new requests to compensate.
|
||||
|
||||
// Retrieve some status variables that we will use to sort requests
|
||||
let peer_list = self.0.fullmesh.get_peer_list();
|
||||
let ring: Arc<Ring> = self.0.ring.borrow().clone();
|
||||
let our_zone = match ring.config.members.get(&self.0.our_node_id) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Augment requests with some information used to sort them.
|
||||
// The tuples are as follows:
|
||||
// (is another node?, is another zone?, latency, node ID, request future)
|
||||
// We store all of these tuples in a vec that we can sort.
|
||||
// By sorting this vec, we priorize ourself, then nodes in the same zone,
|
||||
// and within a same zone we priorize nodes with the lowest latency.
|
||||
let mut requests = requests
|
||||
.map(|(to, fut)| {
|
||||
let peer_zone = match ring.config.members.get(&to) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
let peer_avg_ping = peer_list
|
||||
.iter()
|
||||
.find(|x| x.id.as_ref() == to.as_slice())
|
||||
.map(|pi| pi.avg_ping)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Duration::from_secs(1));
|
||||
(
|
||||
to != self.0.our_node_id,
|
||||
peer_zone != our_zone,
|
||||
peer_avg_ping,
|
||||
to,
|
||||
fut,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort requests by (priorize ourself, priorize same zone, priorize low latency)
|
||||
requests
|
||||
.sort_by_key(|(diffnode, diffzone, ping, _to, _fut)| (*diffnode, *diffzone, *ping));
|
||||
|
||||
// Make an iterator to take requests in their sorted order
|
||||
let mut requests = requests.into_iter();
|
||||
|
||||
// resp_stream will contain all of the requests that are currently in flight.
|
||||
// (for the moment none, they will be added in the loop below)
|
||||
let mut resp_stream = FuturesUnordered::new();
|
||||
|
||||
// Do some requests and collect results
|
||||
'request_loop: while successes.len() < quorum {
|
||||
// If the current set of requests that are running is not enough to possibly
|
||||
// reach quorum, start some new requests.
|
||||
while successes.len() + resp_stream.len() < quorum {
|
||||
if let Some((_, _, _, _to, fut)) = requests.next() {
|
||||
resp_stream.push(fut);
|
||||
} else {
|
||||
// If we have no request to add, we know that we won't ever
|
||||
// reach quorum: bail out now.
|
||||
break 'request_loop;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
assert!(!resp_stream.is_empty()); // because of loop invariants
|
||||
|
||||
// Wait for one request to terminate
|
||||
match resp_stream.next().await.unwrap() {
|
||||
Ok(msg) => {
|
||||
successes.push(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Case 2: all of the requests need to be sent in all cases,
|
||||
// and need to terminate. (this is the case for writes that
|
||||
// must be spread to n nodes)
|
||||
// Just start all the requests in parallel and return as soon
|
||||
// as the quorum is reached.
|
||||
let mut resp_stream = requests
|
||||
.map(|(_, fut)| fut)
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
while let Some(resp) = resp_stream.next().await {
|
||||
match resp {
|
||||
Ok(msg) => {
|
||||
successes.push(msg);
|
||||
if successes.len() >= quorum {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !resp_stream.is_empty() {
|
||||
// Continue remaining requests in background.
|
||||
// Continue the remaining requests immediately using tokio::spawn
|
||||
// but enqueue a task in the background runner
|
||||
// to ensure that the process won't exit until the requests are done
|
||||
// (if we had just enqueued the resp_stream.collect directly in the background runner,
|
||||
// the requests might have been put on hold in the background runner's queue,
|
||||
// in which case they might timeout or otherwise fail)
|
||||
let wait_finished_fut = tokio::spawn(async move {
|
||||
resp_stream.collect::<Vec<Result<_, _>>>().await;
|
||||
});
|
||||
self.0.background.spawn(wait_finished_fut.map(|_| Ok(())));
|
||||
}
|
||||
}
|
||||
|
||||
if results.len() >= quorum {
|
||||
// Continue requests in background.
|
||||
// Continue the remaining requests immediately using tokio::spawn
|
||||
// but enqueue a task in the background runner
|
||||
// to ensure that the process won't exit until the requests are done
|
||||
// (if we had just enqueued the resp_stream.collect directly in the background runner,
|
||||
// the requests might have been put on hold in the background runner's queue,
|
||||
// in which case they might timeout or otherwise fail)
|
||||
if !strategy.rs_interrupt_after_quorum {
|
||||
let wait_finished_fut = tokio::spawn(async move {
|
||||
resp_stream.collect::<Vec<_>>().await;
|
||||
});
|
||||
self.background.spawn(wait_finished_fut.map(|_| Ok(())));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
if successes.len() >= quorum {
|
||||
Ok(successes)
|
||||
} else {
|
||||
let errors = errors.iter().map(|e| format!("{}", e)).collect::<Vec<_>>();
|
||||
Err(Error::Quorum(quorum, results.len(), to.len(), errors))
|
||||
Err(Error::Quorum(quorum, successes.len(), to.len(), errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-12
@@ -235,10 +235,7 @@ impl System {
|
||||
node_status: RwLock::new(HashMap::new()),
|
||||
netapp: netapp.clone(),
|
||||
fullmesh: fullmesh.clone(),
|
||||
rpc: RpcHelper {
|
||||
fullmesh,
|
||||
background: background.clone(),
|
||||
},
|
||||
rpc: RpcHelper::new(netapp.id.into(), fullmesh, background.clone(), ring.clone()),
|
||||
system_endpoint,
|
||||
replication_factor,
|
||||
rpc_listen_addr: config.rpc_bind_addr,
|
||||
@@ -500,13 +497,7 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
let peer_list = self
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
.map(|n| (n.id.into(), n.addr))
|
||||
.collect::<Vec<_>>();
|
||||
if let Err(e) = self.persist_peer_list.save_async(&peer_list).await {
|
||||
if let Err(e) = self.save_peer_list().await {
|
||||
warn!("Could not save peer list to file: {}", e);
|
||||
}
|
||||
|
||||
@@ -520,6 +511,28 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_peer_list(&self) -> Result<(), Error> {
|
||||
// Prepare new peer list to save to file
|
||||
// It is a vec of tuples (node ID as Uuid, node SocketAddr)
|
||||
let mut peer_list = self
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
.map(|n| (n.id.into(), n.addr))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Before doing it, we read the current peer list file (if it exists)
|
||||
// and append it to the list we are about to save,
|
||||
// so that no peer ID gets lost in the process.
|
||||
if let Ok(mut prev_peer_list) = self.persist_peer_list.load_async().await {
|
||||
prev_peer_list.retain(|(id, _ip)| peer_list.iter().all(|(id2, _ip2)| id2 != id));
|
||||
peer_list.extend(prev_peer_list);
|
||||
}
|
||||
|
||||
// Save new peer list to file
|
||||
self.persist_peer_list.save_async(&peer_list).await
|
||||
}
|
||||
|
||||
async fn pull_config(self: Arc<Self>, peer: Uuid) {
|
||||
let resp = self
|
||||
.rpc
|
||||
@@ -543,7 +556,7 @@ impl EndpointHandler<SystemRpc> for System {
|
||||
SystemRpc::Connect(node) => self.handle_connect(node).await,
|
||||
SystemRpc::PullConfig => Ok(self.handle_pull_config()),
|
||||
SystemRpc::AdvertiseStatus(adv) => self.handle_advertise_status(from.into(), adv).await,
|
||||
SystemRpc::AdvertiseConfig(adv) => self.clone().handle_advertise_config(&adv).await,
|
||||
SystemRpc::AdvertiseConfig(adv) => self.clone().handle_advertise_config(adv).await,
|
||||
SystemRpc::GetKnownNodes => Ok(self.handle_get_known_nodes()),
|
||||
_ => Err(Error::BadRpc("Unexpected RPC message".to_string())),
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ where
|
||||
}
|
||||
/// Get a reference to the value assigned to a key
|
||||
pub fn get(&self, k: &K) -> Option<&V> {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(k)) {
|
||||
Ok(i) => Some(&self.vals[i].2),
|
||||
Err(_) => None,
|
||||
}
|
||||
@@ -132,14 +132,14 @@ where
|
||||
{
|
||||
fn merge(&mut self, other: &Self) {
|
||||
for (k, ts2, v2) in other.vals.iter() {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(k)) {
|
||||
Ok(i) => {
|
||||
let (_, ts1, _v1) = &self.vals[i];
|
||||
if ts2 > ts1 {
|
||||
self.vals[i].1 = *ts2;
|
||||
self.vals[i].2 = v2.clone();
|
||||
} else if ts1 == ts2 {
|
||||
self.vals[i].2.merge(&v2);
|
||||
self.vals[i].2.merge(v2);
|
||||
}
|
||||
}
|
||||
Err(i) => {
|
||||
|
||||
@@ -49,7 +49,7 @@ where
|
||||
|
||||
/// Get a reference to the value assigned to a key
|
||||
pub fn get(&self, k: &K) -> Option<&V> {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(k)) {
|
||||
Ok(i) => Some(&self.vals[i].1),
|
||||
Err(_) => None,
|
||||
}
|
||||
@@ -76,9 +76,9 @@ where
|
||||
{
|
||||
fn merge(&mut self, other: &Self) {
|
||||
for (k, v2) in other.vals.iter() {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(k)) {
|
||||
Ok(i) => {
|
||||
self.vals[i].1.merge(&v2);
|
||||
self.vals[i].1.merge(v2);
|
||||
}
|
||||
Err(i) => {
|
||||
self.vals.insert(i, (k.clone(), v2.clone()));
|
||||
|
||||
+2
-2
@@ -167,7 +167,7 @@ where
|
||||
// Calculate an update to apply to this node
|
||||
// This update is an Option<_>, so that it is None if the update is a no-op
|
||||
// and we can thus skip recalculating and re-storing everything
|
||||
let mutate = match self.read_node_txn(tx, &key)? {
|
||||
let mutate = match self.read_node_txn(tx, key)? {
|
||||
MerkleNode::Empty => new_vhash.map(|vhv| MerkleNode::Leaf(k.to_vec(), vhv)),
|
||||
MerkleNode::Intermediate(mut children) => {
|
||||
let key2 = key.next_key(khash);
|
||||
@@ -270,7 +270,7 @@ where
|
||||
};
|
||||
|
||||
if let Some(new_node) = mutate {
|
||||
let hash = self.put_node_txn(tx, &key, &new_node)?;
|
||||
let hash = self.put_node_txn(tx, key, &new_node)?;
|
||||
Ok(Some(hash))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct TableShardedReplication {
|
||||
impl TableReplication for TableShardedReplication {
|
||||
fn read_nodes(&self, hash: &Hash) -> Vec<Uuid> {
|
||||
let ring = self.system.ring.borrow();
|
||||
ring.get_nodes(&hash, self.replication_factor)
|
||||
ring.get_nodes(hash, self.replication_factor)
|
||||
}
|
||||
fn read_quorum(&self) -> usize {
|
||||
self.read_quorum
|
||||
@@ -35,7 +35,7 @@ impl TableReplication for TableShardedReplication {
|
||||
|
||||
fn write_nodes(&self, hash: &Hash) -> Vec<Uuid> {
|
||||
let ring = self.system.ring.borrow();
|
||||
ring.get_nodes(&hash, self.replication_factor)
|
||||
ring.get_nodes(hash, self.replication_factor)
|
||||
}
|
||||
fn write_quorum(&self) -> usize {
|
||||
self.write_quorum
|
||||
|
||||
+2
-2
@@ -266,7 +266,7 @@ where
|
||||
let nodes = self
|
||||
.data
|
||||
.replication
|
||||
.write_nodes(&begin)
|
||||
.write_nodes(begin)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if nodes.contains(&self.system.id) {
|
||||
@@ -530,7 +530,7 @@ where
|
||||
Ok(SyncRpc::RootCkDifferent(hash != *h))
|
||||
}
|
||||
SyncRpc::GetNode(k) => {
|
||||
let node = self.merkle.read_node(&k)?;
|
||||
let node = self.merkle.read_node(k)?;
|
||||
Ok(SyncRpc::Node(k.clone(), node))
|
||||
}
|
||||
SyncRpc::Items(items) => {
|
||||
|
||||
+3
-1
@@ -32,6 +32,8 @@ toml = "0.5"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
http = "0.2"
|
||||
hyper = "0.14"
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum Error {
|
||||
#[error(display = "Tokio join error: {}", _0)]
|
||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
||||
|
||||
#[error(display = "Tokio semaphore acquire error: {}", _0)]
|
||||
TokioSemAcquire(#[error(source)] tokio::sync::AcquireError),
|
||||
|
||||
#[error(display = "Remote error: {}", _0)]
|
||||
RemoteError(String),
|
||||
|
||||
|
||||
@@ -9,3 +9,5 @@ pub mod data;
|
||||
pub mod error;
|
||||
pub mod persister;
|
||||
pub mod time;
|
||||
pub mod token_bucket;
|
||||
pub mod tranquilizer;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub struct TokenBucket {
|
||||
// Replenish rate: number of tokens per second
|
||||
replenish_rate: u64,
|
||||
// Current number of tokens
|
||||
tokens: u64,
|
||||
// Last replenish time
|
||||
last_replenish: Instant,
|
||||
}
|
||||
|
||||
impl TokenBucket {
|
||||
pub fn new(replenish_rate: u64) -> Self {
|
||||
Self {
|
||||
replenish_rate,
|
||||
tokens: 0,
|
||||
last_replenish: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&mut self, tokens: u64) {
|
||||
while self.tokens < tokens {
|
||||
let needed = tokens - self.tokens;
|
||||
let delay = (needed as f64) / (self.replenish_rate as f64);
|
||||
sleep(Duration::from_secs_f64(delay)).await;
|
||||
self.replenish();
|
||||
}
|
||||
self.tokens -= tokens;
|
||||
}
|
||||
|
||||
pub fn replenish(&mut self) {
|
||||
let now = Instant::now();
|
||||
let new_tokens =
|
||||
((now - self.last_replenish).as_secs_f64() * (self.replenish_rate as f64)) as u64;
|
||||
self.tokens += new_tokens;
|
||||
self.last_replenish = now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// A tranquilizer is a helper object that is used to make
|
||||
/// background operations not take up too much time.
|
||||
///
|
||||
/// Background operations are done in a loop that does the following:
|
||||
/// - do one step of the background process
|
||||
/// - tranquilize, i.e. wait some time to not overload the system
|
||||
///
|
||||
/// The tranquilizer observes how long the steps take, and keeps
|
||||
/// in memory a number of observations. The tranquilize operation
|
||||
/// simply sleeps k * avg(observed step times), where k is
|
||||
/// the tranquility factor. For instance with a tranquility of 2,
|
||||
/// the tranquilizer will sleep on average 2 units of time for every
|
||||
/// 1 unit of time spent doing the background task.
|
||||
pub struct Tranquilizer {
|
||||
n_observations: usize,
|
||||
observations: VecDeque<Duration>,
|
||||
sum_observations: Duration,
|
||||
last_step_begin: Instant,
|
||||
}
|
||||
|
||||
impl Tranquilizer {
|
||||
pub fn new(n_observations: usize) -> Self {
|
||||
Self {
|
||||
n_observations,
|
||||
observations: VecDeque::with_capacity(n_observations + 1),
|
||||
sum_observations: Duration::ZERO,
|
||||
last_step_begin: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tranquilize(&mut self, tranquility: u32) {
|
||||
let observation = Instant::now() - self.last_step_begin;
|
||||
|
||||
self.observations.push_back(observation);
|
||||
self.sum_observations += observation;
|
||||
|
||||
while self.observations.len() > self.n_observations {
|
||||
self.sum_observations -= self.observations.pop_front().unwrap();
|
||||
}
|
||||
|
||||
if !self.observations.is_empty() {
|
||||
let delay = (tranquility * self.sum_observations) / (self.observations.len() as u32);
|
||||
sleep(delay).await;
|
||||
}
|
||||
|
||||
self.reset();
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.last_step_begin = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub async fn run_web_server(
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let server = Server::bind(addr).serve(service);
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
||||
info!("Web server listening on http://{}", addr);
|
||||
|
||||
@@ -95,12 +95,12 @@ async fn serve_file(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<
|
||||
// Get path
|
||||
let path = req.uri().path().to_string();
|
||||
let index = &garage.config.s3_web.index;
|
||||
let key = path_to_key(&path, &index)?;
|
||||
let key = path_to_key(&path, index)?;
|
||||
|
||||
info!("Selected bucket: \"{}\", selected key: \"{}\"", bucket, key);
|
||||
|
||||
let res = match *req.method() {
|
||||
Method::HEAD => handle_head(garage, &req, &bucket, &key).await?,
|
||||
Method::HEAD => handle_head(garage, &req, bucket, &key).await?,
|
||||
Method::GET => handle_get(garage, &req, bucket, &key).await?,
|
||||
_ => return Err(Error::BadRequest("HTTP method not supported".to_string())),
|
||||
};
|
||||
@@ -173,7 +173,7 @@ fn host_to_bucket<'a>(host: &'a str, root: &str) -> &'a str {
|
||||
/// When a path ends with "/", we append the index name to match traditional web server behavior
|
||||
/// which is also AWS S3 behavior.
|
||||
fn path_to_key<'a>(path: &'a str, index: &str) -> Result<Cow<'a, str>, Error> {
|
||||
let path_utf8 = percent_encoding::percent_decode_str(&path).decode_utf8()?;
|
||||
let path_utf8 = percent_encoding::percent_decode_str(path).decode_utf8()?;
|
||||
|
||||
if !path_utf8.starts_with('/') {
|
||||
return Err(Error::BadRequest(
|
||||
|
||||
Reference in New Issue
Block a user