Compare commits

..

1 Commits

Author SHA1 Message Date
trinity-1686a 8bbf7e98c9 add internals for supporting transactional updates 2026-02-07 13:26:56 +01:00
258 changed files with 4198 additions and 10906 deletions
+2 -15
View File
@@ -2,14 +2,13 @@ labels:
nix: "enabled" nix: "enabled"
when: when:
- event: event:
- push
- tag - tag
- pull_request - pull_request
- deployment - deployment
- cron - cron
- manual - manual
- event: push
branch: main-*
steps: steps:
- name: check formatting - name: check formatting
@@ -53,15 +52,3 @@ steps:
- nix-build -j4 --attr flakePackages.dev - nix-build -j4 --attr flakePackages.dev
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
depends_on: [ build ] depends_on: [ build ]
- name: helm chart tests
image: helmunittest/helm-unittest:4.2.3-1.1.2
commands:
- helm lint --strict script/helm/garage
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/daemonset.yaml
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/ingress.yaml
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/existing-secret.yaml
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/monitoring.yaml
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/minimal.yaml
- helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
- helm unittest --strict script/helm/garage
-231
View File
@@ -1,231 +0,0 @@
# Contributing to Garage
## Policy on AI
To ensure the quality of the codebase and documentation, the use of AI,
including LLMs and coding agents, is strictly restricted in the following way:
- AI **must not** be used to write documentation
- **Do not** use AI to write bug reports, commit descriptions and pull request
messages
- **Do not** use AI agents to make contributions to Garage, all contributions
must be led by a human that know what they are doing at all times
- AI **may** be used for some tedious code generation tasks, limited to very
mechanical translations from API docs or boilerplate writing. The code
generated must be so simple as to make it clear that it cannot be covered by
copyright.
You are free to make use of AI privately to explore the codebase and solve
conceptual problems, but please restrain from copying the output from an LLM
anywhere in your code or on the issue tracker, or from letting an agent edit
the codebase directly.
## Asking questions
Read the documentation before asking questions.
Do not use the issue tracker to ask questions about Garage.
Questions asked on the issue tracker will be closed.
Ask questions on the Matrix channel `#garage:deuxfleurs.fr` so that any
community member can see your question and help you out.
If you need in-depth support from the Garage developers specifically, write to
`garagehq@deuxfleurs.fr`. Even if you do so, we do not commit to giving you an
answer.
## Reporting bugs
When writing a bug report, use this checklist:
- For bugs that can be reproduced:
- confirm that you are using the latest version of Garage and that the bug still exists in this version
- set the log level to debug using the `RUST_LOG=garage=debug` environment variable and reproduce the bug to get more verbose logs
- Check whether there is already an open issue in the bug tracker. If so, your bug report is still valuable but please add it as a comment to the existing issue instead of opening a new one.
- Collect as much information as possible:
- logs of the Garage daemon at the time the issue happened, including logs that show what was happening before the issue occurred
- the output of `garage status`
- the output of `garage stats -a`
- the output of `garage layout history`
- Write a detailed bug report, including:
- a description of your cluster (number of nodes, hardware, operating system, networking, etc)
- a detailed description of what you did that led to the issue, including any code or command line that invoked a Garage API
- what you were expecting
- what actually happened, and how that's different from what you expected
- the information collected previously
- if possible, simple steps to help the developers reproduce the issue locally
Bug reports that are imprecise or otherwise unactionable will be closed.
## Suggesting new features
Garage can be improved in many ways, but just suggesting a new feature does not mean we will implement it.
Feature requests that may lead to an actual implementation are feature requests that:
- are precise and actionable, i.e. include a precise description of the expected behavior and any necessary architectural details required for the implementation
- are motivated by actual need from a variety of users
Moreover, a certain number of features are defined as out-of-scope for Garage, including but not limited to:
- extensions to the S3 API that are not present on AWS
- features that require the implementation of a consensus algorithm
- more generally, features that are incompatible with the architecture of Garage and its goal of staying simple
Only feature requests in one of the following category may stay open in the issue tracker:
- features that the Garage team wants to work on
- features that are being actively worked on by an external contributor which is clearly identified
- features that are easy to implement and could be an easy task for a new contributor that wants to get to know the codebase
All other feature requests will be closed after a few months of inactivity, so as to keep the number of open issues to a manageable level.
Feature requests that are clearly out of scope will be closed directly.
## Improving the documentation
An easy way to contribute to Garage which also adds a lot of value is to
improve the documentation. Make sure to write in clear technical English, and
write unambiguously. Documentation contributions are very appreciated if they
are well-written.
## For developers
We welcome code contributions to Garage that adhere to our standards for quality:
- Changes should be reviewed from a functional perspective to ensure that they work well with the existing codebase and do not introduce bugs or subtle issues.
- You must have tested your contribution to make sure that it does what it says. The amount of testing required is proportional to the complexity of the change introduced.
- Any new feature must be properly documented following existing practices (see below).
- Unit tests should be included when relevant.
- Contributions should pass basic lints for syntactic quality (`cargo fmt`, `cargo clippy`, `typos`).
- Contributions should pass our CI test suite.
- No user-facing breaking changes may be introduced between major releases.
- No internal data model change may be introduced between major releases, to
ensure that Garage daemons with different minor/patch versions numbers can
work together in a cluster. For major releases, a proper migration path
should be implemented and tested thoroughly.
Please follow up on your work when changes are requested, to avoid stale PRs.
Do not take it personally if a Garage developer pushes directly to your branch
to modify your contribution, as this might be necessary to get it merged
faster.
### Properly documenting your contribution
#### Configuration options
New configuration options should be documented in
`doc/book/reference-manual/configuration.md`. The documentation for a
configuration option should be exhaustive. For instance, for choice options all
choices should be listed explicitly with a precise description of their
meaning.
In terms of syntax, all configuration options should appear in three places:
- in the example at the top, with an example value
- in the index of all configuration options which is sorted by alphabetical order
- in its dedicated subsection with full reference text
#### CLI commands and command flags
CLI commands are self-documented using the doc commends in the codebase.
Make sure to write clear and precise comments for all options you are adding.
#### S3 features
If you implement new S3 features, make sure to update the compatibility matrix in `doc/book/reference-manual/s3-compatibility.md`.
#### Admin API
The admin API has an OpenAPI specification that is automatically generated
using Utoipa, from a description of each endpoint that is given in
`src/api/admin/openapi.rs` and a description of data structure schemas in
`src/api/admin/api.rs`. The code in `openapi.rs` is only used to generate the
OpenAPI specification document and not for the actual implementation in Garage,
whereas structures defined in `api.rs` are also used for the implementation of
API calls. Make sure to write good doc comments for all of these items so that
the OpenAPI specification will be precise and accurate.
An up-to-date version of the OpenAPI specification document should be kept in
the repository in `doc/api/garage-admin-v2.json`. When you are making changes
to the admin API, update this document with the following command:
```
cargo run -- admin-api-schema > doc/api/garage-admin-v2.json
```
## Garage team organization
Alex (handle `lx`) is the lead developer and is responsible of ensuring the
correctness of Garage and stability between version upgrades.
The other maintainers are Trinity (handle `trinity-1686a`), Quentin (handle `quentin`) and Maximilien (handle `halfa`).
Maximilien is responsible for coordinating effort on the Kubernetes integration / Helm chart.
## Pull request merging criteria
The following PRs should only be merged after review and approval from Alex:
- PRs that introduce architectural changes, such as changes in the data model
or change in the coordination protocols between nodes
- PRs that introduce changes on the format of data structures used for
persistent disk storage and internal cluster communication (RPC)
- PRs that are suspected of introducing some kind of breakage or unexpected
behavior due to their complexity
PRs that introduce breaking change for users but don't fall in one of the
previous category should be discussed between maintainers to evaluate the
impact on users when upgrading. Alex's approval is not required to merge them
as long as they are clearly identified as breaking in the PR title, and are
properly merged in the branch for the next major version and not in the current
main branch.
All other PRs can be merged by any maintainer on their own, once they are
confident that the quality standards defined in this document are respected
before merging.
## Merging strategy
When merging PRs, maintainers should ensure that a Git commit is created by
Forgejo that records the PR number, its title and its text in the commit
message. If a PR is fixing an issue, make sure that the issue number is
included in the PR title as well. This is to ensure that when releasing a new
version of Garage, the changelog in the release notes can be properly
constructed by reading the Git log since the last release.
We also want to keep the history "almost linear" to facilitate the use of `git
bisect` if it ever were necessary. This leaves the following two merging
strategies:
- For PRs that consist of many commits that should stay independent, the
"rebase and create merge commit" strategy should be used. The merge commit is
created automatically by Forgejo and saves the PR's number, title and text in
the commit message.
- For PRs that consist of only one commit, or a few number of commits that can
be merged, the "create squash commit" strategy should be used. This way a
single commit will be created by Forgejo which also saves the PR's number,
title and text in the commit message.
When cherry-picking commits from one branch to the other, a simple fast-forward
merging strategy can be used if the commit message already references a PR
number.
Generated
+920 -1478
View File
File diff suppressed because it is too large Load Diff
+65 -106
View File
@@ -16,7 +16,6 @@ members = [
"src/garage", "src/garage",
"src/k2v-client", "src/k2v-client",
"src/format-table", "src/format-table",
"fuzz",
] ]
default-members = ["src/garage"] default-members = ["src/garage"]
@@ -25,131 +24,108 @@ default-members = ["src/garage"]
# Internal Garage crates # Internal Garage crates
format_table = { version = "0.1.1", path = "src/format-table" } format_table = { version = "0.1.1", path = "src/format-table" }
garage_api_common = { version = "2.3.0", path = "src/api/common" } garage_api_common = { version = "2.2.0", path = "src/api/common" }
garage_api_admin = { version = "2.3.0", path = "src/api/admin" } garage_api_admin = { version = "2.2.0", path = "src/api/admin" }
garage_api_s3 = { version = "2.3.0", path = "src/api/s3" } garage_api_s3 = { version = "2.2.0", path = "src/api/s3" }
garage_api_k2v = { version = "2.3.0", path = "src/api/k2v" } garage_api_k2v = { version = "2.2.0", path = "src/api/k2v" }
garage_block = { version = "2.3.0", path = "src/block" } garage_block = { version = "2.2.0", path = "src/block" }
garage_db = { version = "2.3.0", path = "src/db", default-features = false } garage_db = { version = "2.2.0", path = "src/db", default-features = false }
garage_model = { version = "2.3.0", path = "src/model", default-features = false } garage_model = { version = "2.2.0", path = "src/model", default-features = false }
garage_net = { version = "2.3.0", path = "src/net" } garage_net = { version = "2.2.0", path = "src/net" }
garage_rpc = { version = "2.3.0", path = "src/rpc" } garage_rpc = { version = "2.2.0", path = "src/rpc" }
garage_table = { version = "2.3.0", path = "src/table" } garage_table = { version = "2.2.0", path = "src/table" }
garage_util = { version = "2.3.0", path = "src/util" } garage_util = { version = "2.2.0", path = "src/util" }
garage_web = { version = "2.3.0", path = "src/web" } garage_web = { version = "2.2.0", path = "src/web" }
k2v-client = { version = "0.0.4", path = "src/k2v-client" } k2v-client = { version = "0.0.4", path = "src/k2v-client" }
# External crates from crates.io # External crates from crates.io
arc-swap = "1.8" arc-swap = "1.1"
arbitrary = { version = "1.4.2"}
argon2 = "0.5" argon2 = "0.5"
async-trait = "0.1" async-trait = "0.1.7"
backtrace = "0.3" backtrace = "0.3"
base64 = "0.22" base64 = "0.21"
blake2 = "0.10" blake2 = "0.10"
bytes = "1.11" bytes = "1.0"
bytesize = "2.3" bytesize = "1.1"
cfg-if = "1.0" cfg-if = "1.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
crc-fast = "1.9" crc-fast = "1.6"
crypto-common = "0.1" crypto-common = "0.1"
fundu = "2.0" gethostname = "0.4"
fundu-systemd = "0.3" git-version = "0.3.4"
gethostname = "1.1"
git-version = "0.3"
hex = "0.4" hex = "0.4"
hexdump = "0.1" hexdump = "0.1"
html-escape = "0.2.13"
hmac = "0.12" hmac = "0.12"
itertools = "0.14" itertools = "0.12"
ipnet = "2.11" ipnet = "2.9.0"
lazy_static = "1.5" lazy_static = "1.4"
libfuzzer-sys = "0.4"
md-5 = "0.10" md-5 = "0.10"
mktemp = "0.5" mktemp = "0.5"
nix = { version = "0.31", default-features = false, features = ["fs"] } nix = { version = "0.29", default-features = false, features = ["fs"] }
nom = "8.0" nom = "7.1"
parking_lot = "0.12" parking_lot = "0.12"
parse_duration = "2.1"
paste = "1.0" paste = "1.0"
pin-project = "1.1" pin-project = "1.0.12"
pnet_datalink = "0.35" pnet_datalink = "0.34"
rand = "0.9" rand = "0.8"
sha1 = "0.10" sha1 = "0.10"
sha2 = "0.10" sha2 = "0.10"
timeago = { version = "0.5", default-features = false } timeago = { version = "0.4", default-features = false }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
aes-gcm = { version = "0.10", features = ["aes", "stream"] } aes-gcm = { version = "0.10", features = ["aes", "stream"] }
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
kuska-handshake = { version = "0.2.0", features = ["default", "async_std"] } kuska-handshake = { version = "0.2.0", features = ["default", "async_std"] }
clap = { version = "4.5", features = ["derive", "env"] } clap = { version = "4.1", features = ["derive", "env"] }
pretty_env_logger = "0.5" pretty_env_logger = "0.5"
structopt = { version = "0.3", default-features = false } structopt = { version = "0.3", default-features = false }
syslog-tracing = "0.3" syslog-tracing = "0.3"
tracing = "0.1" tracing = "0.1"
tracing-journald = "0.3" tracing-journald = "0.3.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
heed = { version = "0.22", default-features = false, features = [] } heed = { version = "0.11", default-features = false, features = ["lmdb"] }
rusqlite = { version = "0.38", features = ["fallible_uint"] } rusqlite = "0.37"
r2d2 = "0.8" r2d2 = "0.8"
r2d2_sqlite = "0.32" r2d2_sqlite = "0.31"
fjall = "2.11" fjall = "2.4"
async-compression = { version = "0.4", features = ["tokio", "zstd"] } async-compression = { version = "0.4", features = ["tokio", "zstd"] }
zstd = { version = "0.13", default-features = false } zstd = { version = "0.13", default-features = false }
quick-xml = { version = "0.39", features = ["serialize"] } quick-xml = { version = "0.26", features = ["serialize"] }
rmp-serde = "1.3" rmp-serde = "1.1.2"
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
serde_bytes = "0.11" serde_bytes = "0.11"
serde_json = "1.0" serde_json = "1.0"
toml = { version = "0.9", default-features = false, features = ["parse", "serde"] } toml = { version = "0.8", default-features = false, features = ["parse"] }
utoipa = { version = "5.4", features = ["chrono"] } utoipa = { version = "5.3.1", features = ["chrono"] }
# newer version requires rust edition 2021 # newer version requires rust edition 2021
k8s-openapi = { version = "0.27", features = ["v1_35"] } k8s-openapi = { version = "0.21", features = ["v1_24"] }
kube = { version = "3.0", default-features = false, features = [ kube = { version = "0.88", default-features = false, features = ["runtime", "derive", "client", "rustls-tls"] }
"runtime", schemars = "0.8"
"derive", reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-manual-roots", "json"] }
"client",
"rustls-tls",
] }
schemars = "1.2"
reqwest = { version = "0.13", default-features = false, features = [
"rustls-no-provider",
"json",
] }
form_urlencoded = "1.2" form_urlencoded = "1.0.0"
http = "1.4" http = "1.0"
httpdate = "1.0" httpdate = "1.0"
http-range = "0.1" http-range = "0.1"
http-body-util = "0.1" http-body-util = "0.1"
hyper = { version = "1.8", default-features = false } hyper = { version = "1.0", default-features = false }
hyper-util = { version = "0.1", features = ["full"] } hyper-util = { version = "0.1", features = ["full"] }
multer = "3.1" multer = "3.0"
percent-encoding = "2.3" percent-encoding = "2.2"
roxmltree = "0.21" roxmltree = "0.19"
url = "2.5" url = "2.3"
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
tokio = { version = "1.49", default-features = false, features = [ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
"rt",
"rt-multi-thread",
"io-util",
"net",
"time",
"macros",
"sync",
"signal",
"fs",
] }
tokio-util = { version = "0.7", features = ["compat", "io"] } tokio-util = { version = "0.7", features = ["compat", "io"] }
tokio-stream = { version = "0.1", features = ["net"] } tokio-stream = { version = "0.1", features = ["net"] }
socket2 = { version = "0.6", features = ["all"] }
opentelemetry = { version = "0.17", features = ["rt-tokio", "metrics", "trace"] } opentelemetry = { version = "0.17", features = ["rt-tokio", "metrics", "trace"] }
opentelemetry-prometheus = "0.10" opentelemetry-prometheus = "0.10"
@@ -158,42 +134,25 @@ opentelemetry-contrib = "0.9"
prometheus = "0.13" prometheus = "0.13"
# used by the k2v-client crate only # used by the k2v-client crate only
aws-sigv4 = { version = "1.3", default-features = false } aws-sigv4 = { version = "1.1", default-features = false }
hyper-rustls = { version = "0.27", default-features = false, features = [ hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
"http1",
"http2",
"ring",
"rustls-native-certs",
] }
log = "0.4" log = "0.4"
thiserror = "2.0" thiserror = "2.0"
# ---- used only as build / dev dependencies ---- # ---- used only as build / dev dependencies ----
assert-json-diff = "2.0" assert-json-diff = "2.0"
rustc_version = "0.4" rustc_version = "0.4.0"
static_init = "1.0" static_init = "1.0"
aws-smithy-runtime = { version = "1.9", default-features = false, features = [ aws-smithy-runtime = { version = "1.8", default-features = false, features = ["tls-rustls"] }
"tls-rustls", aws-sdk-config = { version = "1.62", default-features = false }
] } aws-sdk-s3 = { version = "1.79", default-features = false, features = ["rt-tokio"] }
aws-sdk-config = { version = "1.99", default-features = false }
aws-sdk-s3 = { version = "1.121", default-features = false, features = [ [profile.dev]
"rt-tokio", #lto = "thin" # disabled for now, adds 2-4 min to each CI build
] } lto = "off"
[profile.release] [profile.release]
lto = "thin" lto = true
codegen-units = 16 codegen-units = 1
opt-level = 3 opt-level = 3
strip = "debuginfo" strip = true
[workspace.lints.clippy]
# pedantic lints configuration
doc_markdown = "warn"
format_collect = "warn"
manual_midpoint = "warn"
semicolon_if_nothing_returned = "warn"
unnecessary_semicolon = "warn"
unnecessary_wraps = "warn"
# nursery lints configuration
# or_fun_call = "warn" # enable it to help detect non trivial code used in `_or` method
-45
View File
@@ -1,45 +0,0 @@
# Governance of Gararge
This documents how the Garage project operates. It reflects the state of the project as of July 2026 and is not optimal. The team is interested to improve it in the future.
## Team organization
* **Contributors**: anyone can contribute by proposing changes in issues and pull requests.
* **Maintainers**: they are responsible for reviewing, merging pull requests, publishing releases and triaging issues.
The current maintainers are:
* Alex (handle `lx`)
* Trinity (handle `trinity-1686a`)
* Quentin (handle `quentin`)
* Maximilien (handle `halfa`), who is in particular responsible for coordinating effort on the Kubernetes integration / Helm chart.
They are added to a white-list of the branch protection rule of the repository to enable them to merge pull requests.
To become a maintainer, you need to be a long-term contributor and earn the personal trust of Alex.
There is no set process for leaving the maintainer role.
* **Lead developer**: Alex (handle `lx`) is the lead developer and is responsible of ensuring the
correctness of Garage and stability between version upgrades. He may transfer this role to someone else as he sees fit.
## Communication channels
The team coordinates in the following channels:
* The issue tracker and pull requests of the official repository.
* The `#garage:deuxfleurs.fr` matrix channel (in English), open to anyone.
On this channel, users may ask for support and discussions about development also happen.
* The `#garage-dev:deuxfleurs.fr` matrix channel (in French), not advertised to contributors but de facto accessible to anyone.
Discussions about development and project coordination happen there.
The moderators for those discussion channels are the Garage maintainers.
## Decision procedures
Decisions are taken by lazy consensus, with the lead developer settling discussions when a consensus cannot be reached.
## Governance changes
There is no set process for changing the governance of garage.
## See also
* [Project goals](https://garagehq.deuxfleurs.fr/documentation/design/goals/)
* [Contributing instructions](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main-v2/CONTRIBUTING.md)
-14
View File
@@ -1,14 +0,0 @@
# Security Reporting
If you wish to report responsibly a security vulnerability about Garage, we ask that you follow the following process.
Please report each security vulnerabilities by filling out the following template:
- PROJECT: A URL to the code repository containing the vulnerable version - be reminded that the source of truth is at https://git.deuxfleurs.fr/deuxfleurs/garage
- PUBLIC: Please let us know if this vulnerability has been made or discussed publicly already, and if so, please let us know where.
- DESCRIPTION: Please provide precise description of the security vulnerability you have found with as much information as you are able and willing to provide.
Please send the above info, along with any other information you feel is pertinent by emailing the core team at: garagehq@deuxfleurs.fr
The Garage Core Team will let you know within a few weeks whether or not your report has been accepted or rejected.
We ask that you please keep the report confidential until we have either responded or made a public announcement.
+13 -658
View File
@@ -12,7 +12,7 @@
"name": "AGPL-3.0", "name": "AGPL-3.0",
"identifier": "AGPL-3.0" "identifier": "AGPL-3.0"
}, },
"version": "v2.3.0" "version": "v2.2.0"
}, },
"servers": [ "servers": [
{ {
@@ -1797,17 +1797,6 @@
"type": "string" "type": "string"
}, },
"description": "Plain-text information about the layout computation\n(do not try to parse this)" "description": "Plain-text information about the layout computation\n(do not try to parse this)"
},
"statistics": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ComputationStat",
"description": "Structured statistics about the layout computation"
}
]
} }
} }
}, },
@@ -2130,180 +2119,6 @@
"Historical" "Historical"
] ]
}, },
"ComputationStat": {
"type": "object",
"required": [
"replicationFactor",
"effectiveZoneRedundancy",
"partitionSize",
"lowPartitionSize",
"usableCapacity",
"totalCapacity",
"effectiveCapacity",
"lowUsableCapacity",
"zones"
],
"properties": {
"effectiveCapacity": {
"type": "integer",
"format": "int64",
"description": "The final effective capacity of the cluster, accounting for replication",
"minimum": 0
},
"effectiveZoneRedundancy": {
"type": "integer",
"description": "The zone redundancy factor achieved by this layout",
"minimum": 0
},
"lowPartitionSize": {
"type": "boolean",
"description": "Warning flag indicating when partitions are very small"
},
"lowUsableCapacity": {
"type": "boolean",
"description": "Warning flag indicating that the raw node capacity could not be used\neffectively"
},
"partitionSize": {
"type": "integer",
"format": "int64",
"description": "The size of a partition, in bytes",
"minimum": 0
},
"previousPartitionSize": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "The size of a partition, in bytes, in the previous layout",
"minimum": 0
},
"replicationFactor": {
"type": "integer",
"description": "The cluster's replication factor",
"minimum": 0
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The total raw capacity of nodes",
"minimum": 0
},
"totalMovedPartitions": {
"type": [
"integer",
"null"
],
"description": "The total number of partitions that will be moved to a new storage node",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The portion of total raw node capacity that is used by partitions",
"minimum": 0
},
"zones": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ComputationStatZone"
},
"description": "Per-zone storage statistics"
}
}
},
"ComputationStatNode": {
"type": "object",
"required": [
"id",
"tags",
"storedPartitions",
"newPartitions",
"totalCapacity",
"usableCapacity"
],
"properties": {
"id": {
"type": "string",
"description": "The node's ID"
},
"newPartitions": {
"type": "integer",
"description": "The number of partitions that are newly replicated on this node",
"minimum": 0
},
"storedPartitions": {
"type": "integer",
"description": "The number of partitions that are replicated on this node",
"minimum": 0
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "The node's tags as defined in the layout"
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The node's raw capacity",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The portion of the node's raw capacity that is used by partitions it stores",
"minimum": 0
}
}
},
"ComputationStatZone": {
"type": "object",
"required": [
"name",
"nodes",
"totalReplicatedPartitions",
"uniquePartitions",
"totalCapacity",
"usableCapacity"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the zone"
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ComputationStatNode"
},
"description": "Per-node storage statistics for nodes in this zone"
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The total raw capacity of nodes in this zone",
"minimum": 0
},
"totalReplicatedPartitions": {
"type": "integer",
"description": "The total number of partition replicas in this zone",
"minimum": 0
},
"uniquePartitions": {
"type": "integer",
"description": "The number of unique partitions that have at least one replica in this zone",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The used portion of the raw capacity of nodes in this zones",
"minimum": 0
}
}
},
"ConnectClusterNodesRequest": { "ConnectClusterNodesRequest": {
"type": "array", "type": "array",
"items": { "items": {
@@ -2525,16 +2340,6 @@
"format": "int64", "format": "int64",
"description": "Total number of bytes used by objects in this bucket" "description": "Total number of bytes used by objects in this bucket"
}, },
"corsRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/cors.Rule"
},
"description": "CORS rules for this bucket"
},
"created": { "created": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@@ -2558,16 +2363,6 @@
}, },
"description": "List of access keys that have permissions granted on this bucket" "description": "List of access keys that have permissions granted on this bucket"
}, },
"lifecycleRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/lifecycle.Rule"
},
"description": "Object lifecycle rules for this bucket"
},
"objects": { "objects": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
@@ -2628,15 +2423,6 @@
}, },
"indexDocument": { "indexDocument": {
"type": "string" "type": "string"
},
"routingRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/website.RoutingRule"
}
} }
} }
}, },
@@ -2795,61 +2581,8 @@
"freeform" "freeform"
], ],
"properties": { "properties": {
"bucketCount": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "number of buckets in the cluster",
"minimum": 0
},
"dataAvail": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "available storage space for object data in the entire cluster, in bytes",
"minimum": 0
},
"freeform": { "freeform": {
"type": "string", "type": "string"
"description": "cluster statistics as a free-form string, kept for compatibility with nodes\nrunning older v2.x versions of garage"
},
"incompleteAvailInfo": {
"type": [
"boolean",
"null"
],
"description": "true if the available storage space statistics are imprecise due to missing\ninformation of disconnected nodes. When this is the case, the actual\nspace available in the cluster might be lower than the reported values."
},
"metadataAvail": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "available storage space for object metadata in the entire cluster, in bytes",
"minimum": 0
},
"totalObjectBytes": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "total size of objects stored in all buckets, before compression, deduplication and\nreplication (this is NOT equivalent to actual disk usage in the cluster)",
"minimum": 0
},
"totalObjectCount": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "total number of objects stored in all buckets",
"minimum": 0
} }
} }
}, },
@@ -3322,8 +3055,7 @@
], ],
"properties": { "properties": {
"dbEngine": { "dbEngine": {
"type": "string", "type": "string"
"description": "database engine used for metadata"
}, },
"garageFeatures": { "garageFeatures": {
"type": [ "type": [
@@ -3332,26 +3064,16 @@
], ],
"items": { "items": {
"type": "string" "type": "string"
}, }
"description": "build-time features enabled for this garage release"
}, },
"garageVersion": { "garageVersion": {
"type": "string", "type": "string"
"description": "garage version running on this node"
},
"hostname": {
"type": [
"string",
"null"
],
"description": "hostname of this node"
}, },
"nodeId": { "nodeId": {
"type": "string" "type": "string"
}, },
"rustVersion": { "rustVersion": {
"type": "string", "type": "string"
"description": "rustc version with which this garage release was compiled"
} }
} }
}, },
@@ -3361,30 +3083,8 @@
"freeform" "freeform"
], ],
"properties": { "properties": {
"blockManagerStats": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/NodeBlockManagerStats",
"description": "block manager statistics"
}
]
},
"freeform": { "freeform": {
"type": "string", "type": "string"
"description": "node statistics as a free-form string, kept for compatibility with nodes\nrunning older v2.x versions of garage"
},
"tableStats": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/NodeTableStats"
},
"description": "metadata table statistics"
} }
} }
}, },
@@ -3685,8 +3385,7 @@
], ],
"properties": { "properties": {
"dbEngine": { "dbEngine": {
"type": "string", "type": "string"
"description": "database engine used for metadata"
}, },
"garageFeatures": { "garageFeatures": {
"type": [ "type": [
@@ -3695,26 +3394,16 @@
], ],
"items": { "items": {
"type": "string" "type": "string"
}, }
"description": "build-time features enabled for this garage release"
}, },
"garageVersion": { "garageVersion": {
"type": "string", "type": "string"
"description": "garage version running on this node"
},
"hostname": {
"type": [
"string",
"null"
],
"description": "hostname of this node"
}, },
"nodeId": { "nodeId": {
"type": "string" "type": "string"
}, },
"rustVersion": { "rustVersion": {
"type": "string", "type": "string"
"description": "rustc version with which this garage release was compiled"
} }
} }
}, },
@@ -3750,30 +3439,8 @@
"freeform" "freeform"
], ],
"properties": { "properties": {
"blockManagerStats": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/NodeBlockManagerStats",
"description": "block manager statistics"
}
]
},
"freeform": { "freeform": {
"type": "string", "type": "string"
"description": "node statistics as a free-form string, kept for compatibility with nodes\nrunning older v2.x versions of garage"
},
"tableStats": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/NodeTableStats"
},
"description": "metadata table statistics"
} }
} }
}, },
@@ -4112,34 +3779,6 @@
} }
} }
}, },
"NodeBlockManagerStats": {
"type": "object",
"required": [
"rcEntries",
"resyncQueueLen",
"resyncErrors"
],
"properties": {
"rcEntries": {
"type": "integer",
"format": "int64",
"description": "number of reference counter entries",
"minimum": 0
},
"resyncErrors": {
"type": "integer",
"format": "int64",
"description": "number of blocks with resync errors",
"minimum": 0
},
"resyncQueueLen": {
"type": "integer",
"format": "int64",
"description": "number of blocks in the resync queue",
"minimum": 0
}
}
},
"NodeResp": { "NodeResp": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -4303,53 +3942,6 @@
} }
] ]
}, },
"NodeTableStats": {
"type": "object",
"required": [
"tableName",
"items",
"merkleItems",
"merkleQueueLen",
"insertQueueLen",
"gcQueueLen"
],
"properties": {
"gcQueueLen": {
"type": "integer",
"format": "int64",
"description": "number of items in the garbage collection queue",
"minimum": 0
},
"insertQueueLen": {
"type": "integer",
"format": "int64",
"description": "number of items in the remote insert queue",
"minimum": 0
},
"items": {
"type": "integer",
"format": "int64",
"description": "number of items stored in metadata table",
"minimum": 0
},
"merkleItems": {
"type": "integer",
"format": "int64",
"description": "size of the merkle tree representing all items in the table",
"minimum": 0
},
"merkleQueueLen": {
"type": "integer",
"format": "int64",
"description": "number of items in the merkle tree update queue",
"minimum": 0
},
"tableName": {
"type": "string",
"description": "name of metadata table"
}
}
},
"NodeUpdateTrackers": { "NodeUpdateTrackers": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -4406,17 +3998,6 @@
"newLayout": { "newLayout": {
"$ref": "#/components/schemas/GetClusterLayoutResponse", "$ref": "#/components/schemas/GetClusterLayoutResponse",
"description": "Details about the new cluster layout" "description": "Details about the new cluster layout"
},
"statistics": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ComputationStat",
"description": "Structured statistics about the layout computation"
}
]
} }
} }
} }
@@ -4536,7 +4117,7 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or\n`UpdateAdminToken` trivially allows for privilege escalation, and is thus\nfunctionally equivalent to granting a scope of `*`." "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or\n`UpdateAdminToken` trivially allows for privilege escalation, and is thus\nfunctionnally equivalent to granting a scope of `*`."
} }
} }
}, },
@@ -4546,24 +4127,6 @@
"UpdateBucketRequestBody": { "UpdateBucketRequestBody": {
"type": "object", "type": "object",
"properties": { "properties": {
"corsRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/cors.Rule"
}
},
"lifecycleRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/lifecycle.Rule"
}
},
"quotas": { "quotas": {
"oneOf": [ "oneOf": [
{ {
@@ -4609,15 +4172,6 @@
"string", "string",
"null" "null"
] ]
},
"routingRules": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/website.RoutingRule"
}
} }
} }
}, },
@@ -4859,205 +4413,6 @@
] ]
} }
] ]
},
"cors.Rule": {
"type": "object",
"required": [
"AllowedOrigin",
"AllowedMethod"
],
"properties": {
"AllowedHeader": {
"type": "array",
"items": {}
},
"AllowedMethod": {
"type": "array",
"items": {}
},
"AllowedOrigin": {
"type": "array",
"items": {}
},
"ExposeHeader": {
"type": "array",
"items": {}
},
"ID": {},
"MaxAgeSeconds": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
}
}
},
"lifecycle.AbortIncompleteMpu": {
"type": "object",
"required": [
"DaysAfterInitiation"
],
"properties": {
"DaysAfterInitiation": {
"$ref": "#/components/schemas/xml.IntValue"
}
}
},
"lifecycle.Expiration": {
"type": "object",
"properties": {
"Date": {},
"Days": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
}
}
},
"lifecycle.Filter": {
"type": "object",
"properties": {
"And": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/lifecycle.Filter"
}
]
},
"ObjectSizeGreaterThan": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
},
"ObjectSizeLessThan": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
},
"Prefix": {}
}
},
"lifecycle.Rule": {
"type": "object",
"required": [
"Status"
],
"properties": {
"AbortIncompleteMultipartUpload": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/lifecycle.AbortIncompleteMpu"
}
]
},
"Expiration": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/lifecycle.Expiration"
}
]
},
"Filter": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/lifecycle.Filter"
}
]
},
"ID": {},
"Status": {}
}
},
"website.Condition": {
"type": "object",
"properties": {
"HttpErrorCodeReturnedEquals": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
},
"KeyPrefixEquals": {}
}
},
"website.Redirect": {
"type": "object",
"properties": {
"HostName": {},
"HttpRedirectCode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/xml.IntValue"
}
]
},
"Protocol": {},
"ReplaceKeyPrefixWith": {},
"ReplaceKeyWith": {}
}
},
"website.RoutingRule": {
"type": "object",
"required": [
"Redirect"
],
"properties": {
"Condition": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/website.Condition"
}
]
},
"Redirect": {
"$ref": "#/components/schemas/website.Redirect"
}
}
},
"xml.IntValue": {
"type": "integer",
"format": "int64"
} }
}, },
"securitySchemes": { "securitySchemes": {
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -547,7 +547,7 @@ ejabberdctl module_install mod_s3_upload
Create the required key and bucket with: Create the required key and bucket with:
```bash ```bash
garage key create ejabberd garage key new --name ejabberd
garage bucket create objects.xmpp-server.fr garage bucket create objects.xmpp-server.fr
garage bucket allow objects.xmpp-server.fr --read --write --key ejabberd garage bucket allow objects.xmpp-server.fr --read --write --key ejabberd
garage bucket website --allow objects.xmpp-server.fr garage bucket website --allow objects.xmpp-server.fr
@@ -678,7 +678,7 @@ For more information on deployment you can check the [ente documentation](https:
This is the usual Garage setup: This is the usual Garage setup:
```bash ```bash
garage key create pleroma-key garage key new --name pleroma-key
garage bucket create pleroma garage bucket create pleroma
garage bucket allow pleroma --read --write --owner --key pleroma-key garage bucket allow pleroma --read --write --owner --key pleroma-key
``` ```
@@ -759,7 +759,7 @@ This feature requires `pict-rs >= 4.0.0`.
This is the usual Garage setup: This is the usual Garage setup:
```bash ```bash
garage key create pictrs-key garage key new --name pictrs-key
garage bucket create pictrs-data garage bucket create pictrs-data
garage bucket allow pictrs-data --read --write --key pictrs-key garage bucket allow pictrs-data --read --write --key pictrs-key
``` ```
+1 -1
View File
@@ -22,7 +22,7 @@ Note that `git-annex` requires to be compiled with Haskell package version
`aws-0.24` to work with Garage. `aws-0.24` to work with Garage.
```bash ```bash
garage key create my-key garage key new --name my-key
garage bucket create my-git-annex garage bucket create my-git-annex
garage bucket allow my-git-annex --read --write --key my-key garage bucket allow my-git-annex --read --write --key my-key
``` ```
+1 -1
View File
@@ -268,7 +268,7 @@ duck --delete garage:/my-files/an-object.txt
## WinSCP (libs3) {#winscp} ## WinSCP (libs3) {#winscp}
*You can find instructions on how to use the GUI in french [in our wiki](https://guide.deuxfleurs.fr/services/winscp/).* *You can find instructions on how to use the GUI in french [in our wiki](https://guide.deuxfleurs.fr/prise_en_main/winscp/).*
How to use `winscp.com`, the CLI interface of WinSCP: How to use `winscp.com`, the CLI interface of WinSCP:
+1 -1
View File
@@ -27,7 +27,7 @@ which support storing metrics in an object store:
This can be configured with Garage with the following: This can be configured with Garage with the following:
```bash ```bash
garage key create vector-system-logs garage key new --name vector-system-logs
garage bucket create system-logs garage bucket create system-logs
garage bucket allow system-logs --read --write --key vector-system-logs garage bucket allow system-logs --read --write --key vector-system-logs
``` ```
-7
View File
@@ -25,13 +25,6 @@ garage bucket website --allow my-website
Now it will be **publicly** exposed on the web endpoint (by default listening on port 3902). Now it will be **publicly** exposed on the web endpoint (by default listening on port 3902).
> The bucket needs to have a *global alias* to be exposed as a website. If the
> bucket was created with `garage bucket create` it will have an alias;
> if created via the S3 API [you will have to manually add the alias
> ](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/742) first.
> Creating globally aliased buckets from the S3 API is [currently under
> discussion](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/649).
## How exposed websites work ## How exposed websites work
Our website serving logic is as follow: Our website serving logic is as follow:
+5 -5
View File
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
## Get a Docker image ## Get a Docker image
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v2.3.0`) and not the `latest` tag. We encourage you to use a fixed tag (eg. `v2.2.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v2.3.0` but it's up to you For this example, we will use the latest published version at the time of the writing which is `v2.2.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
For example: For example:
``` ```
docker pull dxflrs/garage:v2.3.0 sudo docker pull dxflrs/garage:v2.2.0
``` ```
## Deploying and configuring Garage ## Deploying and configuring Garage
@@ -171,7 +171,7 @@ docker run \
-v /etc/garage.toml:/etc/garage.toml \ -v /etc/garage.toml:/etc/garage.toml \
-v /var/lib/garage/meta:/var/lib/garage/meta \ -v /var/lib/garage/meta:/var/lib/garage/meta \
-v /var/lib/garage/data:/var/lib/garage/data \ -v /var/lib/garage/data:/var/lib/garage/data \
dxflrs/garage:v2.3.0 dxflrs/garage:v2.2.0
``` ```
With this command line, Garage should be started automatically at each boot. With this command line, Garage should be started automatically at each boot.
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
version: "3" version: "3"
services: services:
garage: garage:
image: dxflrs/garage:v2.3.0 image: dxflrs/garage:v2.2.0
network_mode: "host" network_mode: "host"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+1 -68
View File
@@ -142,74 +142,7 @@ server {
## Apache httpd ## Apache httpd
The [Apache HTTP Server](https://httpd.apache.org/) @TODO
is a general purpose web server that includes
[reverse proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
capabilities.
### Exposing the S3 endpoints
Create a new [virtual host](https://httpd.apache.org/docs/2.4/vhosts/),
obtain a certificate using
[certbot](https://eff-certbot.readthedocs.io/en/stable/using.html#apache),
and add the
[`ProxyPass`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#proxypass)
and
[`ProxyPreserveHost`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#proxypreservehost)
options:
```apache
<VirtualHost *:443>
ServerName garage.example.com
SSLCertificateFile /etc/letsencrypt/live/garage.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/garage.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Header always set Strict-Transport-Security "max-age=31536000"
Header always add Content-Security-Policy upgrade-insecure-requests
ProxyPass "/" "http://localhost:3900/" nocanon
ProxyPreserveHost on
</VirtualHost>
```
The `nocanon` keyword is important for
[presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html);
otherwise,
> `mod_proxy` will canonicalise ProxyPassed URLs.
> But this may be incompatible with some backends,
> particularly those that make use of `PATH_INFO`.
> The optional `nocanon` keyword suppresses this
> and passes the URL path "raw" to the backend.
### Exposing the web endpoint
Adding static websites backed by Garage works very similarly,
with the only difference being the port selected in the `ProxyPass` directive.
```apache
ProxyPass "/" "http://localhost:3902/" nocanon
```
### Using Unix sockets
Apache can also proxy via Unix sockets instead of TCP ports,
if Garage is so configured.
`garage.toml`:
```toml
[s3_api]
api_bind_addr = "/run/garage/s3_api.socket"
```
Apache config:
```apache
ProxyPass "/" "unix:/run/garage/s3_api.socket|http://localhost/" nocanon
```
## Traefik v2 ## Traefik v2
+23
View File
@@ -82,6 +82,12 @@ nix-build \
*The result is located in `result/bin`. You can pass arguments to cross compile: check `.woodpecker/release.yml` for examples.* *The result is located in `result/bin`. You can pass arguments to cross compile: check `.woodpecker/release.yml` for examples.*
If you modify a `Cargo.toml` or regenerate any `Cargo.lock`, you must run `cargo2nix`:
```
cargo2nix -f
```
Many tools like rclone, `mc` (minio-client), or `aws` (awscliv2) will be available in your environment and will be useful to test Garage. Many tools like rclone, `mc` (minio-client), or `aws` (awscliv2) will be available in your environment and will be useful to test Garage.
**This is the recommended method.** **This is the recommended method.**
@@ -118,6 +124,23 @@ cargo fmt # format the project, run it before any commit!
cargo clippy # run the linter, run it before any commit! cargo clippy # run the linter, run it before any commit!
``` ```
This is specific to our project, but you will need one last tool, `cargo2nix`.
To install it, run:
```bash
cargo install --git https://github.com/superboum/cargo2nix --branch main cargo2nix
```
You must use it every time you modify a `Cargo.toml` or regenerate a `Cargo.lock` file as follow:
```bash
cargo build # Rebuild Cargo.lock if needed
cargo2nix -f
```
It will output a `Cargo.nix` file which is a specific `Cargo.lock` file dedicated to Nix that is required by our CI
which means you must include it in your commits.
Later, to use our scripts and integration tests, you might need additional tools. Later, to use our scripts and integration tests, you might need additional tools.
These tools are listed at the end of the `shell.nix` package in the `nativeBuildInputs` part. These tools are listed at the end of the `shell.nix` package in the `nativeBuildInputs` part.
It is up to you to find a way to install the ones you need on your computer. It is up to you to find a way to install the ones you need on your computer.
@@ -3,6 +3,15 @@ title = "Miscellaneous notes"
weight = 20 weight = 20
+++ +++
## Quirks about cargo2nix/rust in Nix
If you use submodules in your crate (like `crdt` and `replication` in `garage_table`), you must list them in `default.nix`
The Windows target does not work. it might be solvable through [overrides](https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix). Indeed, we pass `x86_64-pc-windows-gnu` but mingw need `x86_64-w64-mingw32`
We have a simple [PR on cargo2nix](https://github.com/cargo2nix/cargo2nix/pull/201) that fixes critical bugs but the project does not seem very active currently. We must use [my patched version of cargo2nix](https://github.com/superboum/cargo2nix) to enable i686 and armv6l compilation. We might need to contribute to cargo2nix in the future.
## Nix ## Nix
Nix has no armv7 + musl toolchains but armv7l is backward compatible with armv6l. Nix has no armv7 + musl toolchains but armv7l is backward compatible with armv6l.
+1 -1
View File
@@ -91,7 +91,7 @@ is definitely lost, then there is no other choice than to declare your S3 object
as unrecoverable, and to delete them properly from the data store. This can be done as unrecoverable, and to delete them properly from the data store. This can be done
using the `garage block purge` command. using the `garage block purge` command.
## Rebalancing data directories {#rebalance} ## Rebalancing data directories
In [multi-HDD setups](@/documentation/operations/multi-hdd.md), to ensure that In [multi-HDD setups](@/documentation/operations/multi-hdd.md), to ensure that
data blocks are well balanced between storage locations, you may run a data blocks are well balanced between storage locations, you may run a
+4 -5
View File
@@ -68,11 +68,10 @@ To rebalance data, two strategies can be used:
secondary directory. This might never end up rebalancing everything if there secondary directory. This might never end up rebalancing everything if there
are data blocks that are only read and never written. are data blocks that are only read and never written.
- Active rebalancing: an operator of a Garage node can [explicitly launch a - Active rebalancing: an operator of a Garage node can explicitly launch a repair
repair procedure](@/documentation/operations/durability-repairs.md#rebalance) procedure that rebalances the data directories, moving all blocks to their
that rebalances the data directories, moving all blocks to their primary primary location. Once done, all secondary locations for all hash slices are
location. Once done, all secondary locations for all hash slices are removed removed so that they won't be checked anymore when looking for a data block.
so that they won't be checked anymore when looking for a data block.
## Read-only storage locations ## Read-only storage locations
+137 -215
View File
@@ -43,10 +43,12 @@ or if you want a build customized for your system,
you can [build Garage from source](@/documentation/cookbook/from-source.md). you can [build Garage from source](@/documentation/cookbook/from-source.md).
If none of these option work for you, you can also run Garage in a Docker If none of these option work for you, you can also run Garage in a Docker
container. For simplicity, a minimal command to launch Garage using Docker is container. When using Docker, the commands used in this guide will not work
provided in this quick start guide. We recommend reading the tutorial on anymore. We recommend reading the tutorial on [configuring a
[configuring a multi-node cluster](@/documentation/cookbook/real-world.md) to multi-node cluster](@/documentation/cookbook/real-world.md) to learn about
learn about the full Docker workflow for Garage. using Garage as a Docker container. For simplicity, a minimal command to launch
Garage using Docker is provided in this quick start guide as well.
## Configuring and starting Garage ## Configuring and starting Garage
@@ -80,6 +82,9 @@ bind_addr = "[::]:3902"
root_domain = ".web.garage.localhost" root_domain = ".web.garage.localhost"
index = "index.html" index = "index.html"
[k2v_api]
api_bind_addr = "[::]:3904"
[admin] [admin]
api_bind_addr = "[::]:3903" api_bind_addr = "[::]:3903"
admin_token = "$(openssl rand -base64 32)" admin_token = "$(openssl rand -base64 32)"
@@ -90,13 +95,10 @@ EOF
See the [Configuration file format](https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/) See the [Configuration file format](https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/)
for complete options and values. for complete options and values.
By default, Garage looks for its configuration file in **`/etc/garage.toml`.** Now that your configuration file has been created, you may save it to the directory of your choice.
Since we have written our configuration file in the working directory, we will have to set By default, Garage looks for **`/etc/garage.toml`.**
the following environment variable: You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
at each invocation of the `garage` binary (for example: `garage -c ./garage.toml server`, `garage -c ./garage.toml status`).
```bash
export GARAGE_CONFIG_FILE=$(pwd)/garage.toml
```
As you can see, the `rpc_secret` is a 32 bytes hexadecimal string. As you can see, the `rpc_secret` is a 32 bytes hexadecimal string.
You can regenerate it with `openssl rand -hex 32`. You can regenerate it with `openssl rand -hex 32`.
@@ -109,41 +111,15 @@ Garage server will not be persistent. Change these to locations on your local di
your data to be persisted properly. your data to be persisted properly.
### Configuring initial access credentials
Since `v2.3.0`, Garage can automatically create a default access key and a default storage bucket,
based on values provided in environment variables.
To use this feature, export the following environment variables:
```bash
export GARAGE_DEFAULT_ACCESS_KEY="GK$(openssl rand -hex 16)"
export GARAGE_DEFAULT_SECRET_KEY="$(openssl rand -hex 32)"
export GARAGE_DEFAULT_BUCKET="default-bucket"
```
The example above creates a random access key ID and associated secret key.
You can also provide an access key ID and secret key of your own.
### Launching the Garage server ### Launching the Garage server
Use the following command to launch the Garage server: Use the following command to launch the Garage server:
```bash ```
garage server --single-node --default-bucket garage -c path/to/garage.toml server
``` ```
- the `--single-node` flag instructs Garage to automatically configure a If you have placed the `garage.toml` file in `/etc` (its default location), you can simply run `garage server`.
single-node cluster without data replication;
- the `--default-bucket` flag instructs Garage to create a default access key
and a default bucket using the environment variables we defined above (it
implies `--default-access-key`).
> You can refer to the [manual configuration
> steps](#manual-configuration) if:
>
> - you decide to no use these optional flags;
> - you are running an **older version of Garage (before v2.3.0)**.
Alternatively, if you cannot or do not wish to run the Garage binary directly, Alternatively, if you cannot or do not wish to run the Garage binary directly,
you may use Docker to run Garage in a container using the following command: you may use Docker to run Garage in a container using the following command:
@@ -151,58 +127,21 @@ you may use Docker to run Garage in a container using the following command:
```bash ```bash
docker run \ docker run \
-d \ -d \
--name garage-container \ --name garaged \
-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903 \ -p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903 \
-v $(pwd)/garage.toml:/etc/garage.toml \ -v /path/to/garage.toml:/etc/garage.toml \
-e GARAGE_DEFAULT_ACCESS_KEY \ -v /path/to/garage/meta:/var/lib/garage/meta \
-e GARAGE_DEFAULT_SECRET_KEY \ -v /path/to/garage/data:/var/lib/garage/data \
-e GARAGE_DEFAULT_BUCKET \ dxflrs/garage:v2.2.0
dxflrs/garage:v2.3.0
/garage server --single-node --default-bucket
``` ```
Note that this command will NOT create persistent volumes for Garage's data, so Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
your cluster will be wiped if the container terminates. To persist Garage's
data, you must manually add volumes for the `data` and `metadata` directories
and configure their correct paths in your `garage.toml` files (see [configuring
a multi-node cluster](@/documentation/cookbook/real-world.md)).
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`. #### Troubleshooting
### Checking that Garage runs correctly
The `garage` utility is also used as a CLI tool to administrate your Garage
deployment. It needs read access to your configuration file and to the metadata directory
to obtain connection parameters to contact the local Garage node.
Use the following command to show the status of your cluster:
```
garage status
```
If you are running Garage in a Docker container, you can use the following command instead:
```bash
docker exec garage-container /garage status
```
This should show something like this:
```
==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail Version
563e1ac825ee3323 linuxbox 127.0.0.1:3901 [default] dc1 19.9 GiB 19.5 GiB (97.6%) v2.3.0
```
### Troubleshooting
Ensure your configuration file, `metadata_dir` and `data_dir` are readable by the user running the `garage` server or Docker. Ensure your configuration file, `metadata_dir` and `data_dir` are readable by the user running the `garage` server or Docker.
When running the `garage` CLI, ensure that the path to your configuration file is correctly specified (see below), You can tune Garage's verbosity by setting the `RUST_LOG=` environment variable. \
and that it can read it and read from your metadata directory.
You can tune Garage's verbosity by setting the `RUST_LOG=` environment variable.
Available log levels are (from less verbose to more verbose): `error`, `warn`, `info` *(default)*, `debug` and `trace`. Available log levels are (from less verbose to more verbose): `error`, `warn`, `info` *(default)*, `debug` and `trace`.
```bash ```bash
@@ -215,135 +154,36 @@ Log level `info` is the default value and is recommended for most use cases.
Log level `debug` can help you check why your S3 API calls are not working. Log level `debug` can help you check why your S3 API calls are not working.
### Checking that Garage runs correctly
## Uploading and downloading from Garage The `garage` utility is also used as a CLI tool to configure your Garage deployment.
It uses values from the TOML configuration file to find the Garage daemon running on the
local node, therefore if your configuration file is not at `/etc/garage.toml` you will
again have to specify `-c path/to/garage.toml` at each invocation.
This section will show how to download and upload files on Garage using a third-party tool named `awscli`. If you are running Garage in a Docker container, you can set `alias garage="docker exec -ti <container name> /garage"`
to use the Garage binary inside your container.
If the `garage` CLI is able to correctly detect the parameters of your local Garage node,
### Install and configure `awscli` the following command should be enough to show the status of your cluster:
If you have python on your system, you can install it with:
```bash
python -m pip install --user awscli
```
Now that `awscli` is installed, you must configure it to talk to your Garage
instance using the credentials defined above. Here is a simple way to create
a configuration file in `~/.awsrc` using a single command that will save the
secrets from your environment:
```bash
cat > ~/.awsrc <<EOF
export AWS_ENDPOINT_URL='http://localhost:3900'
export AWS_DEFAULT_REGION='garage'
export AWS_ACCESS_KEY_ID='$GARAGE_DEFAULT_ACCESS_KEY'
export AWS_SECRET_ACCESS_KEY='$GARAGE_DEFAULT_SECRET_KEY'
aws --version
EOF
``` ```
garage status
Note that you need to have at least `awscli` `>=1.29.0` or `>=2.13.0`, otherwise you
need to specify `--endpoint-url` explicitly on each `awscli` invocation.
Now, each time you want to use `awscli` on this target, run:
```bash
source ~/.awsrc
``` ```
*You can create multiple files with different names if you This should show something like this:
have multiple Garage clusters or different keys.
Switching from one cluster to another is as simple as
sourcing the right file.*
### Example usage of `awscli`
```bash
# list buckets
aws s3 ls
# list objects of a bucket
aws s3 ls s3://default-bucket
# copy from your filesystem to garage
aws s3 cp /proc/cpuinfo s3://default-bucket/cpuinfo.txt
# copy from garage to your filesystem
aws s3 cp s3://default-bucket/cpuinfo.txt /tmp/cpuinfo.txt
```
Note that you can use `awscli` for more advanced operations like
creating a bucket, pre-signing a request or managing your website.
[Read the full documentation to know more](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html).
Some features are however not implemented like ACL or policy.
Check [our S3 compatibility list](@/documentation/reference-manual/s3-compatibility.md).
### Other tools for interacting with Garage
The following tools can also be used to send and receive files from/to Garage:
- [minio-client](@/documentation/connect/cli.md#minio-client)
- [s3cmd](@/documentation/connect/cli.md#s3cmd)
- [rclone](@/documentation/connect/cli.md#rclone)
- [Cyberduck](@/documentation/connect/cli.md#cyberduck)
- [WinSCP](@/documentation/connect/cli.md#winscp)
An exhaustive list is maintained in the ["Integrations" > "Browsing tools" section](@/documentation/connect/_index.md).
## Manual configuration {#manual-configuration}
This section provides instructions that are equivalent to using the
`--single-node` and `--default-bucket` flags for automatic configuration. If
you are using an older version of Garage (before v2.3.0), you must follow
these instructions as automatic configuration is not available.
We will have to run quite a few `garage` administration commands to get started.
If you ever get lost, don't forget that the `help` command and the `--help` flags can help you anywhere,
the CLI tool is self-documented! Two examples:
```
garage help
garage bucket allow --help
```
### Configuring the `garage` CLI
Remember that the `garage` CLI needs to know the path of your `garage.toml` configuration file.
If it is not in the default location of `/etc/garage.toml`, you can specify it either:
- by setting the `GARAGE_CONFIG_FILE` environment variable;
- by adding the `-c` flag to each `garage` command, for example: `garage -c ./garage.toml status`.
If you are running Garage in a Docker container, you can set the following alias
to provide a fake `garage`command that uses the Garage binary inside your container:
```bash
alias garage="docker exec -ti <container name> /garage"
```
You can test that your `garage` CLI is configured correctly by running a basic command such as `garage status`.
### Creating a cluster layout
When you first start a cluster without automatic configuration, the output of `garage status` will look as follows:
``` ```
==== HEALTHY NODES ==== ==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail Version ID Hostname Address Tag Zone Capacity
563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED v2.3.0 563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED
``` ```
Creating a cluster layout for a Garage deployment means informing Garage of the ## Creating a cluster layout
disk space available on each node of the cluster using the `-c` flag, as well
as the name of the zone (e.g. datacenter) each machine is located in using the Creating a cluster layout for a Garage deployment means informing Garage
`-z` flag. of the disk space available on each node of the cluster, `-c`,
as well as the name of the zone (e.g. datacenter), `-z`, each machine is located in.
For our test deployment, we are have only one node with zone named `dc1` and a For our test deployment, we are have only one node with zone named `dc1` and a
capacity of `1G`, though the capacity is ignored for a single node deployment capacity of `1G`, though the capacity is ignored for a single node deployment
@@ -364,29 +204,38 @@ garage layout apply --version 1
``` ```
### Creating buckets and keys ## Creating buckets and keys
In this section, we will suppose that we want to create a bucket named `nextcloud-bucket`
that will be accessed through a key named `nextcloud-app-key`.
Don't forget that `help` command and `--help` subcommands can help you anywhere,
the CLI tool is self-documented! Two examples:
```
garage help
garage bucket allow --help
```
### Create a bucket
Let's take an example where we want to deploy NextCloud using Garage as the Let's take an example where we want to deploy NextCloud using Garage as the
main data storage. We will suppose that we want to create a bucket named main data storage.
`nextcloud-bucket` that will be accessed through a key named
`nextcloud-app-key`.
#### Create a bucket First, create a bucket with the following command:
First, create the bucket with the following command:
``` ```
garage bucket create nextcloud-bucket garage bucket create nextcloud-bucket
``` ```
Check that the bucket was created properly: Check that everything went well:
``` ```
garage bucket list garage bucket list
garage bucket info nextcloud-bucket garage bucket info nextcloud-bucket
``` ```
#### Create an API key ### Create an API key
The `nextcloud-bucket` bucket now exists on the Garage server, The `nextcloud-bucket` bucket now exists on the Garage server,
however it cannot be accessed until we add an API key with the proper access rights. however it cannot be accessed until we add an API key with the proper access rights.
@@ -409,14 +258,14 @@ Secret key: 7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34
Authorized buckets: Authorized buckets:
``` ```
Check that the key was created properly: Check that everything works as intended:
``` ```
garage key list garage key list
garage key info nextcloud-app-key garage key info nextcloud-app-key
``` ```
#### Allow a key to access a bucket ### Allow a key to access a bucket
Now that we have a bucket and a key, we need to give permissions to the key on the bucket: Now that we have a bucket and a key, we need to give permissions to the key on the bucket:
@@ -435,5 +284,78 @@ You can check at any time the allowed keys on your bucket with:
garage bucket info nextcloud-bucket garage bucket info nextcloud-bucket
``` ```
You should now be able to read and write objects to the bucket using the
credentials created above. ## Uploading and downloading from Garage
To download and upload files on garage, we can use a third-party tool named `awscli`.
### Install and configure `awscli`
If you have python on your system, you can install it with:
```bash
python -m pip install --user awscli
```
Now that `awscli` is installed, you must configure it to talk to your Garage instance,
with your key. There are multiple ways to do that, the simplest one is to create a file
named `~/.awsrc` with this content:
```bash
export AWS_ACCESS_KEY_ID=xxxx # put your Key ID here
export AWS_SECRET_ACCESS_KEY=xxxx # put your Secret key here
export AWS_DEFAULT_REGION='garage'
export AWS_ENDPOINT_URL='http://localhost:3900'
aws --version
```
Note you need to have at least `awscli` `>=1.29.0` or `>=2.13.0`, otherwise you
need to specify `--endpoint-url` explicitly on each `awscli` invocation.
Now, each time you want to use `awscli` on this target, run:
```bash
source ~/.awsrc
```
*You can create multiple files with different names if you
have multiple Garage clusters or different keys.
Switching from one cluster to another is as simple as
sourcing the right file.*
### Example usage of `awscli`
```bash
# list buckets
aws s3 ls
# list objects of a bucket
aws s3 ls s3://nextcloud-bucket
# copy from your filesystem to garage
aws s3 cp /proc/cpuinfo s3://nextcloud-bucket/cpuinfo.txt
# copy from garage to your filesystem
aws s3 cp s3://nextcloud-bucket/cpuinfo.txt /tmp/cpuinfo.txt
```
Note that you can use `awscli` for more advanced operations like
creating a bucket, pre-signing a request or managing your website.
[Read the full documentation to know more](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html).
Some features are however not implemented like ACL or policy.
Check [our s3 compatibility list](@/documentation/reference-manual/s3-compatibility.md).
### Other tools for interacting with Garage
The following tools can also be used to send and receive files from/to Garage:
- [minio-client](@/documentation/connect/cli.md#minio-client)
- [s3cmd](@/documentation/connect/cli.md#s3cmd)
- [rclone](@/documentation/connect/cli.md#rclone)
- [Cyberduck](@/documentation/connect/cli.md#cyberduck)
- [WinSCP](@/documentation/connect/cli.md#winscp)
An exhaustive list is maintained in the ["Integrations" > "Browsing tools" section](@/documentation/connect/_index.md).
+2 -6
View File
@@ -56,11 +56,10 @@ tls_skip_verify = false
service_name = "garage-daemon" service_name = "garage-daemon"
ca_cert = "/etc/consul/consul-ca.crt" ca_cert = "/etc/consul/consul-ca.crt"
# for `agent` API mode, unset client_cert and client_key:
client_cert = "/etc/consul/consul-client.crt" client_cert = "/etc/consul/consul-client.crt"
client_key = "/etc/consul/consul-key.crt" client_key = "/etc/consul/consul-key.crt"
# optionally enable `token` for authentication: # for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
# token = "abcdef-01234-56789" # token = "abcdef-01234-56789"
tags = [ "dns-enabled" ] tags = [ "dns-enabled" ]
@@ -175,9 +174,6 @@ they do not exist in the configuration file:
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`) Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
instead of printing to stderr. instead of printing to stderr.
- `NO_COLOR` (since `v2.4.0`): set this to `0` or `false` to disable
ANSI color codes in Garage's logs.
The following environment variables can be used to override the corresponding The following environment variables can be used to override the corresponding
values in the configuration file: values in the configuration file:
@@ -451,7 +447,7 @@ If this value is set, Garage will automatically take a snapshot of the metadata
DB file at a regular interval and save it in the metadata directory, DB file at a regular interval and save it in the metadata directory,
or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set. or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set.
This parameter can take any duration string that can be parsed by This parameter can take any duration string that can be parsed by
the [`fundu_systemd`](https://docs.rs/fundu-systemd) crate. the [`parse_duration`](https://docs.rs/parse_duration/latest/parse_duration/#syntax) crate.
Snapshots can allow to recover from situations where the metadata DB file is Snapshots can allow to recover from situations where the metadata DB file is
corrupted, for instance after an unclean shutdown. See [this corrupted, for instance after an unclean shutdown. See [this
+6 -5
View File
@@ -8,11 +8,12 @@ which is an alternative storage API designed to help efficiently store
many small values in buckets (in opposition to S3 which is more designed many small values in buckets (in opposition to S3 which is more designed
to store large blobs). to store large blobs).
K2V is included in release builds since version 0.8.0. Precompiled builds K2V is currently disabled at compile time in all builds, as the
of earlier versions including `k2v` can be found in our download page under specification is still subject to changes. To build a Garage version with
"Extra builds": they can be easily identified as their tag name ends with K2V, the Cargo feature flag `k2v` must be activated. Special builds with
`-k2v` (example: `v0.7.2-k2v`). Otherwise, when compiling Garage, the Cargo the `k2v` feature flag enabled can be obtained from our download page under
feature flag `k2v` must be activated. "Extra builds": such builds can be identified easily as their tag name ends
with `-k2v` (example: `v0.7.2-k2v`).
The specification of the K2V API can be found The specification of the K2V API can be found
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md). [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
-188
View File
@@ -1,188 +0,0 @@
+++
title = "Known issues"
weight = 80
+++
Issues in each section are roughly sorted by order of decreasing impact, based on actual reports from users.
## Architectural limitations
Issues that are caused by design decisions of Garage internals, and that can't
be fixed without major architectural changes in the codebase.
### Metadata performance issues with many objects
**Related issues:**
- [#851 - Performances collapse with 10 millions pictures in a bucket](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/851)
- [#1222 - Cluster Setup Write Performance Degraded After Writing 10 Million Object (200-300Kb per object)](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1222)
### Very big objects cause performance degradation
For each object, there is a single metadata entry called a `Version` that
contains a list of all of the data blocks in the object. For very big objects,
this entry can contain thousands of block references. During the uploading of
an object, this metadata entry needs to be read, deserialized, reserialized and
written for each individual data block uploaded. This means that the
complexity of an upload is `O(n²)` in the number of blocks needed.
This manifests by excessive metadata I/O and CPU usage, and uploads eventually stalling.
**Mitigation:** Increase the `block_size` configuration parameter to reduce the
number of blocks. Make sure multipart uploads use chunks that are at least
`block_size` in size, and that are an exact multiple of `block_size` to avoid
the creation of smaller blocks.
**Long-term solution:** An architectural change in the metadata system would be
required to store block lists in many independent metadata entries instead of
one single big entry per object.
**Related issues:**
- [#662 - Large Files fail to upload](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/662)
- [#1366 - High CPU usage and performance degradation during long multipart uploads](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1366)
### No conditional writes / locking / WORM support (`if-none-match`, ...)
This is structurally impossible to implement in Garage due to the lack of a consensus algorithm,
which is one of Garage's core design choices which we cannot reconsider.
A semi-working, *unsafe* implementation of WORM and object locking could be
implemented, with the following constraint: only after the completion of the
first write (in case of WORM) or the setting of a lock (for object lock) can we
guarantee that the object cannot be overwritten. In case where an overwrite
requests arrives at the same time as the initial request to write or to lock
the object, we cannot implement a safe and consistent way to reject it. This
means that many practical use-cases for `if-none-match` cannot be supported
(e.g. using it to implement mutual exclusion between concurrent writers).
**Related issues:**
- [#1052 - Support conditional writes](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1052)
- [#1127 - Feature Request: WORM (Write Once Read Many) / Object Lock Support](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1127)
### `CreateBucket` race condition
Also due to the lack of a consensus algorithm, there is no mutual exclusion
between concurrent `CreateBucket` requests using the same bucket name.
**Related issues:**
- [#649 - Race condition in CreateBucket](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/649)
### Metadata and data have the same replication factor
There is a single `replication_factor` in the configuration file that applies both to data blocks and metadata entries.
This makes clusters with `replication_factor = 1` particularly vulnerable in cases of metadata corruption (see below), as there
is a single copy of the metadata for each object even in multi-node clusters.
**Mitigation:** Do not use `replication_factor = 1`.
**Long-term solution:** We want to allow scenarios such as replicating the
metadata on 2, 3 or more nodes and the data on only 1 or 2 nodes (for example),
so that the metadata can benefit from better redundancy without increasing the
storage costs for the entire dataset. This will require some important changes
in the codebase.
**Related issues:**
- [#720 - Separate replication modes for metadata/data](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/720)
### Node count limitation
Garage will have issues in clusters with too many nodes, it will not be able to
spread data uniformly among nodes and some nodes will fill up faster than
other. This starts to manifest when the number of nodes is bigger than `10 ×
replication_factor`. This is due to the fact that Garage uses only 256
partitions internally.
**Mitigation:** Build clusters with fewer, bigger nodes.
**Potential solution:** This can be fixed by increasing the number of
partitions in Garage. The code paths exist, there is [a `const`
somewhere](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/6fd9bba0cb55062cb1725ab961b7fa8acb9dcc61/src/rpc/layout/mod.rs#L35)
that theoretically allows to increase the number of partitions up to `2^16`,
but this has not been tested so there might be bugs.
### Buckets are not sharded
For each bucket, the first metadata layer that contains an index of all objects
is not sharded. This index, which includes the names and all metadata (size,
headers, ...) for each object, is stored on `$replication_factor` nodes.
For instance with `replication_factor = 3`, a given bucket will use only 3
specific nodes for this index (chosen at random when the bucket is created) to
store this index. In a multi-zone deployments, these nodes will be spread in
different zones. Each bucket uses a different set of 3 random nodes for its
index.
As a consequence, very large buckets might cause uneven load distribution
within a cluster. If all of the requests on a cluster are for objects in a
single bucket, then the `$replication_factor` nodes that store the index will
become a hotspot in the cluster, with more intensive metadata access patterns.
There is no way of choosing which nodes will have this role.
Currently, we have no report of this being an issue in practice.
**Mitigation:** This impacts in particular clusters that are used for a single
purpose with a single bucket. This can be solved by dividing your dataset among
many buckets, using a client-side sharding strategy that you will have to
design. Use at least as many buckets as you have nodes on your cluster.
## Bugs
Known bugs that are complex to diagnose and fix, and therefore have not been
fixed yet.
### LMDB metadata corruption
Many users have reported situations where the LMDB metadata db becomes
corrupted, sometimes after a forced shutdown of Garage or in case of power
loss. A corrupted database file is generally not recoverable.
**Mitigation:** Use a `replication_factor` of at least 2. Configure automatic
snapshotting using `metadata_auto_snapshot_interval` so that in case of
corruption you can rollback to a working database.
Note that taking filesystem-level snapshots of your `metadata_dir`, although it
is much faster and less I/O intensive than Garage's built-in snapshotting, does
not ensure that the snapshot will be consistent. If the snapshot is taking
during a metadata write, the snapshot itself might be corrupted and thus not
usable as a rollback point. Therefore, prefer using
`metadata_auto_snapshot_interval` in all cases.
### Layout updates might require manual intervention
In case of disconnected nodes, when changing the cluster layout to remove these
nodes and add other nodes instead, Garage might not be able to properly evict
the old nodes from the system. This is a built-in security measure to avoid any
inconsistent cluster states.
This manifests by several cluster layout versions staying active even after a
full resync. You can diagnose this situation with `garage layout history`,
which will give you instructions to fix it.
### Tag assignment
In the `garage layout assign` command, the `-t` argument has to be repeated
multiple times to set multiple tags on a node. Writing multiple tags separated
by commas will result in a single string.
## General footguns
Choices made by the developers that users must be aware of if they don't want
to run into potential issues.
### Resync tranquility is conservative by default
By default, the worker parameters `resync-tranquility` and `resync-worker-count` are set to very conservative values, to avoid overloading nodes with I/O when data needs to be resynchronized between nodes.
This can cause issues where the resync queue grows faster than it can be cleared, which in turn causes performance issues in the rest of Garage.
This situation is indicated by a big resync queue with few resync errors (the queue is not caused by a disconnected/malfunctionning node).
To fix it, increase the number of resync workers and reduce the resync tranquility. For instance, if you want to resync as fast as possible:
```
garage worker set -a resync-worker-count 8
garage worker set -a resync-tranquility 0
```
-47
View File
@@ -166,25 +166,6 @@ that map to zeroes. Note that we need to filter out values from nodes that are
no longer part of the cluster layout, as when nodes are removed they won't no longer part of the cluster layout, as when nodes are removed they won't
necessarily have had the time to set their counters to zero. necessarily have had the time to set their counters to zero.
### Consistency guarantees
K2V provides the following consistency guarantees:
**Read after Write**. After a write has been acknowledged (the request returned
successfully), a subsequent read is guaranteed to contain the value that was
written.
**Monotonic Reads**. Two sequential reads will return values in an order that is
consistent with the order in which they are written (e.g. by concurrent writes).
For example, consider a scenario where a value is set initially set to 0 and a
request writing 1 is performed. Doing two subsequent reads concurrently with the
write is guaranteed to return either `0`, `0` or `0`,`1` or `1`,`1`, but not
`1`,`0`.
It is also possible to perform non-monotonic reads (allowing this last
behavior), which are slightly faster than monotonic reads. This is done by
passing a dedicated flag to read operations (see the endpoints documentation).
## Important details ## Important details
**THIS SECTION CONTAINS A FEW WARNINGS ON THE K2V API WHICH ARE IMPORTANT **THIS SECTION CONTAINS A FEW WARNINGS ON THE K2V API WHICH ARE IMPORTANT
@@ -229,12 +210,6 @@ Query parameters:
|------------|---------------|----------------------------------| |------------|---------------|----------------------------------|
| `sort_key` | **mandatory** | The sort key of the item to read | | `sort_key` | **mandatory** | The sort key of the item to read |
Headers:
| name | default value | meaning |
|-------------------------------|---------------|------------------------------------------|
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
Returns the item with specified partition key and sort key. Values can be Returns the item with specified partition key and sort key. Values can be
returned in either of two ways: returned in either of two ways:
@@ -350,12 +325,6 @@ Query parameters:
The timeout can be set to any number of seconds, with a maximum of 600 seconds (10 minutes). The timeout can be set to any number of seconds, with a maximum of 600 seconds (10 minutes).
Headers:
| name | default value | meaning |
|-------------------------------|---------------|------------------------------------------|
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
**InsertItem: `PUT /<bucket>/<partition key>?sort_key=<sort_key>`** **InsertItem: `PUT /<bucket>/<partition key>?sort_key=<sort_key>`**
@@ -552,14 +521,6 @@ HTTP/1.1 204 NO CONTENT
Batch read of triplets in a bucket. Batch read of triplets in a bucket.
Headers:
| name | default value | meaning |
|-------------------------------|---------------|------------------------------------------|
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
Body:
The request body is a JSON list of searches, that each specify a range of The request body is a JSON list of searches, that each specify a range of
items to get (to get single items, set `singleItem` to `true`). A search is a items to get (to get single items, set `singleItem` to `true`). A search is a
JSON struct with the following fields: JSON struct with the following fields:
@@ -750,14 +711,6 @@ HTTP/1.1 200 OK
Polls a range of items for changes. Polls a range of items for changes.
Headers:
| name | default value | meaning |
|-------------------------------|---------------|------------------------------------------|
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
Body:
The query body is a JSON object consisting of the following fields: The query body is a JSON object consisting of the following fields:
| name | default value | meaning | | name | default value | meaning |
-18
View File
@@ -1,18 +0,0 @@
*
!*.txt
!*.md
!assets
!.gitignore
!*.svg
!*.png
!*.jpg
!*.tex
!Makefile
!.gitignore
!assets/*.drawio.pdf
talk.{nav,out,snm,toc,aux,log}
!talk.pdf
-3
View File
@@ -1,3 +0,0 @@
talk.pdf: talk.tex
pdflatex talk.tex
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-330
View File
@@ -1,330 +0,0 @@
%\nonstopmode
\documentclass[aspectratio=169]{beamer}
\usepackage[utf8]{inputenc}
% \usepackage[frenchb]{babel}
\usepackage{amsmath}
\usepackage{mathtools}
\usepackage{breqn}
\usepackage{multirow}
\usetheme{boxes}
\usepackage{graphicx}
%\useoutertheme[footline=authortitle,subsection=false]{miniframes}
\beamertemplatenavigationsymbolsempty
\definecolor{TitleOrange}{RGB}{255,137,0}
\setbeamercolor{title}{fg=TitleOrange}
\setbeamercolor{frametitle}{fg=TitleOrange}
\definecolor{ListOrange}{RGB}{255,145,5}
\setbeamertemplate{itemize item}{\color{ListOrange}$\blacktriangleright$}
\definecolor{verygrey}{RGB}{70,70,70}
\setbeamercolor{normal text}{fg=verygrey}
\usepackage{tabu}
\usepackage{multicol}
\usepackage{vwcol}
\usepackage{stmaryrd}
\usepackage{graphicx}
\usepackage[normalem]{ulem}
\title{Garage Object Storage: 2.0 update and best practices}
\subtitle{a new storage platform for self-hosted geo-distributed clusters}
\author{Maximilien Richer, Deuxfleurs}
\date{FOSDEM '26}
\begin{document}
\begin{frame}
\centering
\includegraphics[width=.3\linewidth]{../../sticker/Garage.pdf}
\vspace{1em}
{\large\bf Maximilien Richer, Deuxfleurs}
\vspace{1em}
\url{https://garagehq.deuxfleurs.fr/}
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
\end{frame}
\begin{frame}
\frametitle{Our objective at Deuxfleurs}
\begin{center}
French association promoting digital sovereignty and privacy\\
through self-hosting hosting \textbf{as an alternative to large cloud providers}
\end{center}
\vspace{2em}
\vspace{2em}
\begin{center}
\textbf{This requires \underline{resilience}}\\
{\footnotesize (we want good uptime/availability with low supervision)}
\end{center}
\end{frame}
\begin{frame}
\frametitle{But what is Garage, exactly?}
\textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}\\
\vspace{.5em}
that implements resilience through geographical redundancy on commodity hardware
\begin{center}
\includegraphics[width=.8\linewidth]{assets/garageuses.png}
\end{center}
\end{frame}
\begin{frame}
\frametitle{What makes Garage different?}
\textbf{Coordination-free:}
\vspace{2em}
\begin{itemize}
\item No Raft or Paxos
\vspace{1em}
\item Internal data types are CRDTs
\vspace{1em}
\item All nodes are equivalent (no master/leader/index node)
\end{itemize}
\vspace{2em}
$\to$ less sensitive to higher latencies between nodes
\end{frame}
\begin{frame}
\frametitle{What makes Garage different?}
\begin{center}
TODO update with latest garage and minio versions
\includegraphics[width=.9\linewidth]{assets/endpoint-latency-dc.png}
\end{center}
\end{frame}
\begin{frame}
\frametitle{What makes Garage different?}
\textbf{Consistency model:}
\vspace{2em}
\begin{itemize}
\item Not ACID (not required by S3 spec) / not linearizable
\vspace{1em}
\item \textbf{Read-after-write consistency}\\
{\footnotesize (stronger than eventual consistency)}
\end{itemize}
\end{frame}
\begin{frame}
\frametitle{What makes Garage different?}
\textbf{Location-aware:}
\vspace{2em}
\begin{center}
\includegraphics[width=\linewidth]{assets/location-aware.png}
\end{center}
\vspace{2em}
Garage replicates data on different zones when possible
\end{frame}
\begin{frame}
\frametitle{What makes Garage different?}
\begin{center}
\includegraphics[width=.8\linewidth]{assets/map.png}
\end{center}
\end{frame}
\begin{frame}
\frametitle{An ever-increasing compatibility list}
\begin{center}
\includegraphics[width=.7\linewidth]{assets/compatibility.png}
\end{center}
\end{frame}
\begin{frame}
\frametitle{Version history and roadmap}
\begin{itemize}
\item v0.3: initial beta release (2021)
\item v0.7: first released version (2022)
\item v1.0: stable release (2024), will be deprecated in summer 2026 1y after v2.0 was released
\item v2.0: stable release (2025)
\begin{itemize}
\item new HTTP admin API
\item reworded replication configuration: \texttt{replication\_mode} changed to \texttt{replication\_factor} \& \texttt{consistency\_policy}
\end{itemize}
\item
\end{itemize}
\begin{center}
v3.0: TBA may include versionning support, tag on buckets and objets, retention policies...
\end{center}
\end{frame}
\begin{frame}
\centering
{\large\bf Best practices for Garage deployments}
\end{frame}
\begin{frame}
\frametitle{Things you should know}
\begin{itemize}
\item no TLS support, use your own proxy
\item no anonymous access (use website endpoint)
\item you need to assign roles to nodes manually
\item the replication factor cannot be changed easily
\item the default region is \texttt{garage} and not \texttt{us-east-1}
\item only use the \texttt{degraded} consistency policy for data recovery!
\end{itemize}
\end{frame}
\begin{frame}
\frametitle{What hardware should I use?}
\begin{itemize}
\item do NOT use network file storage (NFS, SMB, etc.) for \texttt{\/metadata}
\item get a \textbf{write-intensive flash disk} for the \texttt{\/metadata} folder
\item set \texttt{metadata} on a RAID1 if possible, with a COW filesystem (e.g. Btrfs or ZFS)
\item get large HDDs for the \texttt{\/data} folder
\item use XFS and garage multi-hdd mode for best performance
\item you can use a RAID for data but you'll leave a lot of performance on the table
\end{itemize}
\center\textit{Garage doesn't require a powerful CPUs nor much RAM, but your performance will depend on your disks!}
\end{frame}
\begin{frame}
\frametitle{Picking a metadata engine}
All files-to-block mappings are stored in the metadata engine, including bucket and object metadata. Files below 3KB are stored directly in the metadata engine.
\vspace{1em}
\begin{itemize}
\item Sled: removed in 1.x, move to SQLite or LMDB
\item \textbf{SQLite}: safer, \textbf{recommended for small clusters and single-node}
\item LMDB: faster, recommended for large clusters with metadata redundancy
\begin{itemize}
\item Warning: limited to 480 bytes per key with LMDB (not an issue in practice)
\end{itemize}
\item Fjall: experimental but promising rust-native engine, test it and let us know!
\end{itemize}
\center{Metadata engine can be set node per node, and changed later with a migration tool}
\end{frame}
\begin{frame}
\frametitle{Single-node deployment}
\begin{itemize}
\item garage was initially designed for multi-node deployments
\item single-node deployments are possible, but you will lose resilience
\item \textbf{If you do please ensure you have backups} (especially for metadata)
\begin{itemize}
\item set up \texttt{metadata\_auto\_snapshot\_interval}
\end{itemize}
\item use sqlite to minimize data loss risks on powercuts
\item or use a UPS!
\end{itemize}
\vspace{1em}
Use \texttt{github.com/bikeshedder/garage-single-node} for an easy single-node setup!
\end{frame}
\begin{frame}
\frametitle{Multi-node deployment}
\begin{itemize}
\item try to have geo-distributed zones
\item multiple nodes per zone to add more capacity
\item at least 3 zones for best resilience
\item keep in mind your available network and IO bandwidth
\item \textbf{Rebalancing a cluster can take multiple weeks with large HDDs and slow network links}
\item monitor your nodes with Prometheus + Grafana
\end{itemize}
\center{Deuxfleurs has been running a 9TB (3TB usable) 8-nodes cluster (3+3+2) over retail fiber (10ms site-to-site latency) for close to 5 years now. We heard there are petabyte clusters out there!}
\end{frame}
\begin{frame}
\frametitle{Deploying and administering garage at scale}
\begin{itemize}
\item deploy with your favorite tool (eg. Ansible) and system manager (eg. systemd)
\item or use Docker, docker-compose, Kubernetes or Nomad
\item Kubernetes and Consul are supported for node-to-node discovery
\begin{itemize}
\item you'll still have to manage the layout manually!
\end{itemize}
\item use gateway nodes to optimize network usage
\item ajust \texttt{resync-tranquility} and \texttt{scrub-tranquility} to your ressources
\end{itemize}
\center{Kubernetes storage controller: \texttt{github.com/bmarinov/garage-storage-controller}}
\end{frame}
\begin{frame}
\frametitle{Community UI available!}
\begin{center}
\includegraphics[width=0.9\linewidth]{assets/community-ui.png}\\
\vspace{-1em}
\url{https://github.com/khairul169/garage-webui}
\end{center}
\end{frame}
\begin{frame}
\frametitle{Official Embedded UI comming later this year!}
\begin{center}
\includegraphics[width=0.9\linewidth]{assets/Garage Web Admin - Dashboard@2x.png}\\
\vspace{-1em}
\end{center}
\end{frame}
\begin{frame}
\frametitle{Official Embedded UI comming this year!}
\begin{center}
\includegraphics[width=0.9\linewidth]{assets/Garage Web Admin - Bucket details page@2x.png}\\
\vspace{-1em}
\end{center}
\end{frame}
\begin{frame}
\frametitle{How to make sense of garage metrics?}
\begin{center}
\includegraphics[width=0.7\linewidth]{assets/garage-stats.png}\\
\vspace{-1em}
\end{center}
\end{frame}
\begin{frame}
\frametitle{What if things go wrong?}
\begin{itemize}
\item set logs to debug with \texttt{RUST_LOG=garage_api_common=debug,garage_api_s3=debug,garage=debug}
\item auth issues: check your reverse proxy configuration
\item slow resync: check your network and disk IO usage, and \texttt{resync-tranquility} worker configuration
\item big LMDB database: stop garage and compact with \texttt{mdb\_copy -c}
\item ask us on matrix \texttt{\#garage:deuxfleurs.fr} or open an issue on git.deuxfleurs.fr!
\begin{itemize}
\item provide the output of \texttt{garage status}, \texttt{garage stats} and relevant metrics and logs
\end{itemize}
\end{itemize}
\end{frame}
\begin{frame}
\frametitle{Moving from Minio}
\begin{itemize}
\item list your buckets and your keys
\item create buckets and keys on the garage cluster
\begin{itemize}
\item you cannot import non-garage keys yet, patch to come soon!
\end{itemize}
\item loop over buckets, copy with rclone
\begin{itemize}
\item see doc \url{https://garagehq.deuxfleurs.fr/documentation/connect/cli/}
\end{itemize}
\item blog post coming soon!
\end{itemize}
\end{frame}
\begin{frame}
\frametitle{Demo time!}
\end{frame}
\begin{frame}
\frametitle{Get Garage now!}
\begin{center}
\includegraphics[width=.3\linewidth]{../../logo/garage_hires.png}\\
\vspace{-1em}
\url{https://garagehq.deuxfleurs.fr/}\\
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
\vspace{2em}
\includegraphics[width=.09\linewidth]{assets/rust_logo.png}
\includegraphics[width=.2\linewidth]{assets/AGPLv3_Logo.png}
\end{center}
\end{frame}
\end{document}
%% vim: set ts=4 sw=4 tw=0 noet spelllang=fr :
Generated
+4 -4
View File
@@ -81,17 +81,17 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776914043, "lastModified": 1763952169,
"narHash": "sha256-qug5r56yW1qOsjSI99l3Jm15JNT9CvS2otkXNRNtrPI=", "narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3", "rev": "ab726555a9a72e6dc80649809147823a813fa95b",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3", "rev": "ab726555a9a72e6dc80649809147823a813fa95b",
"type": "github" "type": "github"
} }
}, },
+2 -10
View File
@@ -6,9 +6,9 @@
inputs.nixpkgs.url = inputs.nixpkgs.url =
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632"; "github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
# Rust overlay as of 2026-04-23 # Rust overlay as of 2025-11-24
inputs.rust-overlay.url = inputs.rust-overlay.url =
"github:oxalica/rust-overlay/2d35c4358d7de3a0e606a6e8b27925d981c01cc3"; "github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
# Crane as of 2025-01-24 # Crane as of 2025-01-24
@@ -95,14 +95,6 @@
killall killall
]; ];
}; };
# dev shell for fuzzing
fuzz = pkgs.mkShell {
buildInputs = with pkgs; [
targets.toolchainNightly
cargo-fuzz
];
};
}; };
}); });
} }
-4
View File
@@ -1,4 +0,0 @@
target
corpus
artifacts
coverage
-73
View File
@@ -1,73 +0,0 @@
[package]
name = "garage-fuzz"
version = "0.0.0"
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { workspace = true, features = ["derive"]}
libfuzzer-sys = { workspace = true }
garage_db.workspace = true
garage_table.workspace = true
garage_util.workspace = true
garage_model = { workspace = true, default-features = false, features = ["arbitrary", "k2v"] }
[[bin]]
name = "version_crdt"
path = "fuzz_targets/version_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "mpu_crdt"
path = "fuzz_targets/mpu_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "bucket_crdt"
path = "fuzz_targets/bucket_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "block_ref_crdt"
path = "fuzz_targets/block_ref_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "admin_api_token_crdt"
path = "fuzz_targets/admin_api_token_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "key_crdt"
path = "fuzz_targets/key_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "bucket_alias_crdt"
path = "fuzz_targets/bucket_alias_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "k2v_item_crdt"
path = "fuzz_targets/k2v_item_crdt.rs"
test = false
doc = false
bench = false
-11
View File
@@ -1,11 +0,0 @@
# Fuzzing
## Setup
Install cargo fuzz: `cargo install cargo-fuzz`
## Launch
Run `cargo fuzz run <fuzz_target>` where `<fuzz_target>` is the name (without extension) of one of the `.rs` files in the `fuzz_targets` directory.
If you launch the command outside of the fuzz directory, you need to force the nightly toolchain with `cargo +nightly`.
-38
View File
@@ -1,38 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::admin_token_table::{AdminApiToken, AdminApiTokenParams, AdminApiTokenScope};
use garage_model::permission::ExpirationTime;
use garage_util::crdt;
use libfuzzer_sys::fuzz_target;
type Input = (
bool,
crdt::Lww<String>,
crdt::Lww<crdt::MergingOption<ExpirationTime>>,
crdt::Lww<AdminApiTokenScope>,
);
fn make(input: Input) -> AdminApiToken {
let (deleted, name, expiration, scope) = input;
let state = if deleted {
crdt::Deletable::Deleted
} else {
crdt::Deletable::present(AdminApiTokenParams {
created: 0,
token_hash: String::new(),
name,
expiration,
scope,
})
};
AdminApiToken {
prefix: String::new(),
state,
}
}
fuzz_target!(|inputs: (Input, Input, Input)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
-20
View File
@@ -1,20 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::block_ref_table::BlockRef;
use libfuzzer_sys::fuzz_target;
/// Build a BlockRef with a fixed block hash and version UUID so that CRDT state
/// can be compared across merge results. Only the deleted flag varies.
fn make_block_ref(deleted: bool) -> BlockRef {
BlockRef {
block: [0u8; 32].into(),
version: [0u8; 32].into(),
deleted: deleted.into(),
}
}
fuzz_target!(|inputs: (bool, bool, bool)| {
let (d1, d2, d3) = inputs;
check_crdt_laws(make_block_ref(d1), make_block_ref(d2), make_block_ref(d3));
});
-25
View File
@@ -1,25 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::bucket_alias_table::BucketAlias;
use garage_util::data::Uuid;
use libfuzzer_sys::fuzz_target;
/// Build a BucketAlias with a fixed name so that CRDT state can be compared
/// across merge results. The timestamp and optional bucket ID are the CRDT state.
fn make_bucket_alias(ts: u64, bucket_id: Option<[u8; 32]>) -> BucketAlias {
BucketAlias::new(String::new(), ts, bucket_id.map(Uuid::from))
}
fuzz_target!(|inputs: (
(u64, Option<[u8; 32]>),
(u64, Option<[u8; 32]>),
(u64, Option<[u8; 32]>)
)| {
let ((ts1, b1), (ts2, b2), (ts3, b3)) = inputs;
check_crdt_laws(
make_bucket_alias(ts1, b1),
make_bucket_alias(ts2, b2),
make_bucket_alias(ts3, b3),
);
});
-22
View File
@@ -1,22 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::bucket_table::{Bucket, BucketParams};
use garage_util::crdt::{self, Deletable};
use libfuzzer_sys::fuzz_target;
fn make(state: Deletable<BucketParams>) -> Bucket {
Bucket {
id: [0u8; 32].into(),
state,
}
}
fuzz_target!(|inputs: (
crdt::Deletable<BucketParams>,
crdt::Deletable<BucketParams>,
crdt::Deletable<BucketParams>
)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
-36
View File
@@ -1,36 +0,0 @@
#![no_main]
use std::collections::BTreeMap;
use garage_fuzz::check_crdt_laws;
use garage_model::k2v::item_table::{DvvsEntry, DvvsValue, K2VItem};
use libfuzzer_sys::fuzz_target;
// Timestamps are encoded as `(ts << 32) | shift` so that items built with different
// shifts (0, 1, 2) have disjoint timestamp spaces that still interleave in the sorted merge.
fn make(raw: BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>, shift: u32) -> K2VItem {
let shift = shift as u64;
let items = raw
.into_iter()
.map(|(node, (t_discard, values))| {
let entry = DvvsEntry::from_raw(
(t_discard as u64) << 32 | shift,
values
.into_iter()
.map(|(ts, v)| ((ts as u64) << 32 | shift, v))
.collect(),
);
(node, entry)
})
.collect();
K2VItem::with_raw_items(items)
}
fuzz_target!(|inputs: (
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a, 0), make(b, 1), make(c, 2));
});
-43
View File
@@ -1,43 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::key_table::{Key, KeyParams};
use garage_model::permission::{BucketKeyPerm, ExpirationTime};
use garage_util::crdt;
use garage_util::data::Uuid;
use libfuzzer_sys::fuzz_target;
type Input = (
bool,
crdt::Lww<String>,
crdt::Lww<crdt::MergingOption<ExpirationTime>>,
crdt::Lww<bool>,
crdt::Map<Uuid, BucketKeyPerm>,
crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
);
fn make(input: Input) -> Key {
let (deleted, name, expiration, allow_create_bucket, authorized_buckets, local_aliases) = input;
let state = if deleted {
crdt::Deletable::Deleted
} else {
crdt::Deletable::present(KeyParams {
created: None,
secret_key: String::new(),
name,
expiration,
allow_create_bucket,
authorized_buckets,
local_aliases,
})
};
Key {
key_id: String::new(),
state,
}
}
fuzz_target!(|inputs: (Input, Input, Input)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
-37
View File
@@ -1,37 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::mpu_table::{MpuPart, MpuPartKey, MultipartUpload};
use libfuzzer_sys::fuzz_target;
/// Build a MultipartUpload from an arbitrary deleted flag and parts list, using a fixed
/// upload_id/bucket_id/key so that CRDT state can be compared across merge results.
/// `MpuPart.version` is fixed to a constant since it is identity data, not CRDT state:
/// two replicas of the same part (same MpuPartKey) always share the same version UUID.
/// If deleted, parts are cleared to ensure a valid initial CRDT state.
fn make_mpu(deleted: bool, parts: Vec<(MpuPartKey, MpuPart)>) -> MultipartUpload {
let mut mpu = MultipartUpload::new(
[0u8; 32].into(),
0,
[0u8; 32].into(),
String::new(),
deleted,
);
for (key, mut part) in parts {
part.version = [0u8; 32].into();
mpu.parts.put(key, part);
}
if mpu.deleted.get() {
mpu.parts.clear();
}
mpu
}
fuzz_target!(|inputs: (
(bool, Vec<(MpuPartKey, MpuPart)>),
(bool, Vec<(MpuPartKey, MpuPart)>),
(bool, Vec<(MpuPartKey, MpuPart)>)
)| {
let ((d1, p1), (d2, p2), (d3, p3)) = inputs;
check_crdt_laws(make_mpu(d1, p1), make_mpu(d2, p2), make_mpu(d3, p3));
});
-42
View File
@@ -1,42 +0,0 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::version_table::{Version, VersionBacklink, VersionBlock, VersionBlockKey};
use libfuzzer_sys::fuzz_target;
/// Build a Version from an arbitrary deleted flag and block list, using a fixed uuid/backlink
/// so that CRDT state can be compared across merge results.
/// Duplicate block keys are dropped before construction.
/// If deleted, blocks are cleared to ensure a valid initial CRDT state.
fn make_version(deleted: bool, mut blocks: Vec<(VersionBlockKey, VersionBlock)>) -> Version {
blocks.sort_by_key(|(k, _)| *k);
blocks.dedup_by_key(|(k, _)| *k);
let mut v = Version::new(
[0u8; 32].into(),
VersionBacklink::Object {
bucket_id: [0u8; 32].into(),
key: String::new(),
},
deleted,
);
for (key, block) in blocks {
v.blocks.put(key, block);
}
if v.deleted.get() {
v.blocks.clear();
}
v
}
fuzz_target!(|inputs: (
(bool, Vec<(VersionBlockKey, VersionBlock)>),
(bool, Vec<(VersionBlockKey, VersionBlock)>),
(bool, Vec<(VersionBlockKey, VersionBlock)>)
)| {
let ((d1, b1), (d2, b2), (d3, b3)) = inputs;
check_crdt_laws(
make_version(d1, b1),
make_version(d2, b2),
make_version(d3, b3),
);
});
-2
View File
@@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"
-56
View File
@@ -1,56 +0,0 @@
use garage_table::crdt::Crdt;
use std::fmt::Debug;
pub fn check_crdt_laws<T>(a: T, b: T, c: T)
where
T: Crdt + PartialEq + Clone + Debug,
{
// Idempotency: merge(a, a) == a
{
let mut a2 = a.clone();
a2.merge(&a);
assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}");
}
// Commutativity: merge(a, b) == merge(b, a)
let ab = {
let mut t = a.clone();
t.merge(&b);
t
};
let ba = {
let mut t = b.clone();
t.merge(&a);
t
};
assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}");
// LX's corrolary: merge(merge(a,b),b) = merge(a,b)
let ab_b = {
let mut t = ab.clone();
t.merge(&b);
t
};
assert_eq!(ab, ab_b);
// Associativity: merge(merge(a, b), c) == merge(a, merge(b, c))
let ab_c = {
let mut t = ab;
t.merge(&c);
t
};
let bc = {
let mut t = b;
t.merge(&c);
t
};
let a_bc = {
let mut t = a;
t.merge(&bc);
t
};
assert_eq!(
ab_c, a_bc,
"merge is not associative: {ab_c:#?} != {a_bc:#?}"
);
}
+1 -9
View File
@@ -48,7 +48,7 @@ let
inherit (pkgs) lib stdenv; inherit (pkgs) lib stdenv;
toolchainFn = (p: p.rust-bin.stable."1.95.0".default.override { toolchainFn = (p: p.rust-bin.stable."1.91.0".default.override {
targets = lib.optionals (target != null) [ rustTarget ]; targets = lib.optionals (target != null) [ rustTarget ];
extensions = [ extensions = [
"rust-src" "rust-src"
@@ -148,14 +148,6 @@ let
in rec { in rec {
toolchain = toolchainFn pkgs; toolchain = toolchainFn pkgs;
toolchainNightly = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
targets = lib.optionals (target != null) [ rustTarget ];
extensions = [
"rust-src"
"rustfmt"
];
});
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = [ buildInputs = [
toolchain toolchain
-2
View File
@@ -21,5 +21,3 @@
.idea/ .idea/
*.tmproj *.tmproj
.vscode/ .vscode/
# helm-unittest test suites
tests/
+3 -5
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: garage name: garage
description: S3-compatible object store for small self-hosted geo-distributed deployments description: S3-compatible object store for small self-hosted geo-distributed deployments
type: application type: application
version: 0.9.4 version: 0.9.2
appVersion: "v2.3.0" appVersion: "v2.2.0"
home: https://garagehq.deuxfleurs.fr/ home: https://garagehq.deuxfleurs.fr/
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
@@ -15,6 +15,4 @@ keywords:
sources: sources:
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git - https://git.deuxfleurs.fr/Deuxfleurs/garage.git
maintainers: maintainers: []
- name: Garage maintainer team
email: garagehq@deuxfleurs.fr
+1 -4
View File
@@ -1,6 +1,6 @@
# garage # garage
![Version: 0.9.3](https://img.shields.io/badge/Version-0.9.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.3.0](https://img.shields.io/badge/AppVersion-v2.3.0-informational?style=flat-square) ![Version: 0.9.2](https://img.shields.io/badge/Version-0.9.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.2.0](https://img.shields.io/badge/AppVersion-v2.2.0-informational?style=flat-square)
S3-compatible object store for small self-hosted geo-distributed deployments S3-compatible object store for small self-hosted geo-distributed deployments
@@ -33,14 +33,11 @@ S3-compatible object store for small self-hosted geo-distributed deployments
| garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor | | garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor |
| garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode | | garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode |
| garage.metadataAutoSnapshotInterval | string | `""` | If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval | | garage.metadataAutoSnapshotInterval | string | `""` | If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval |
| garage.admin.apiBindAddr | string | `"[::]:3903"` | |
| garage.rpcBindAddr | string | `"[::]:3901"` | | | garage.rpcBindAddr | string | `"[::]:3901"` | |
| garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object | | garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object |
| garage.s3.api.bindAddr | string | `"[::]:3900"` | |
| garage.s3.api.region | string | `"garage"` | | | garage.s3.api.region | string | `"garage"` | |
| garage.s3.api.rootDomain | string | `".s3.garage.tld"` | | | garage.s3.api.rootDomain | string | `".s3.garage.tld"` | |
| garage.s3.web.index | string | `"index.html"` | | | garage.s3.web.index | string | `"index.html"` | |
| garage.s3.web.bindAddr | string | `"[::]:3902"` | |
| garage.s3.web.rootDomain | string | `".web.garage.tld"` | | | garage.s3.web.rootDomain | string | `".web.garage.tld"` | |
| image.pullPolicy | string | `"IfNotPresent"` | | | image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image | | image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image |
-331
View File
@@ -1,331 +0,0 @@
# An "everything and the kitchen sink" values file for the helm chart: combines many non-default
# settings at once, including examples for the fields that default to empty in
# values.yaml and are therefore hard to guess the expected shape of.
#
# Aside the documentation value, it doubles as an integration-test fixture:
# CI renders and lints the chart with this file (see .woodpecker/debug.yaml)
# to catch feature interactions that per-feature fixtures wouldn't exercise together
# (e.g. both ingresses enabled at once, monitoring + custom service account,
# a DaemonSet-incompatible field set alongside a StatefulSet, ...).
#
# Try it locally with:
# helm template script/helm/garage -f script/helm/garage/complex-values.yaml
# helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
# -- Additional labels to add to all resources created by this chart
commonLabels:
app.kubernetes.io/part-of: storage
team: platform-infrastructure
# Garage configuration. Values under this are written to garage.toml
garage:
# -- sqlite for durability, lmdb for performance
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine
dbEngine: "sqlite"
# -- Here set to 10MiB
# An increase can result in better performance in certain scenarios
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size
blockSize: "10485760"
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor
replicationFactor: "5"
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode
consistencyMode: "dangerous"
# -- zstd compression level of stored blocks
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level
compressionLevel: "5"
# -- If this value is set, Garage will automatically take a snapshot of the metadata DB file and save it in the metadata directory.
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval
metadataAutoSnapshotInterval: "30 days"
rpcBindAddr: "[::]:3901"
# -- If not given, a random secret will be generated and stored in a Secret object
rpcSecret: ""
# -- If you want to provide an rpcSecret within an existing k8s secret,
# specify the secret name here, and store the value under the secret key `rpcSecret`
# ! the default secret will not be created
existingRpcSecret: ""
# -- This is not required if you use the integrated kubernetes discovery. Each
# entry is "<garage_node_id>@<host>:<port>", where <garage_node_id> is the node's public key
# (shown by `garage node id` on that node).
bootstrapPeers:
- "563e1ac825ee3323aa441e72c26d1030d6d4222c43c986812dbf7cd47d18aef@garage-0.garage-headless:3901"
- "86f0f26ae4afbd59aaf9cfb302af3fe0464f2f7b5b21f80f7e6f4e9989b5c1f8@garage-1.garage-headless:3901"
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
# of the helm chart, for example if you operate at namespace level without cluster resources
kubernetesSkipCrd: true
s3:
api:
bindAddr: "[::]:3900"
region: "garage"
rootDomain: ".s3.garage.tld"
web:
bindAddr: "[::]:3902"
rootDomain: ".web.garage.tld"
index: "index.html"
admin:
apiBindAddr: "[::]:3903"
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
additionalTopLevelConfig: |-
data_fsync = true
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
# if set, ignores garage.toml
existingConfigMap: ""
# -- String Template for the garage configuration.
# if set, ignores every other garage.* value above and is rendered with `tpl`,
# so it can reference .Values/.Release/.Chart, e.g.:
# garageTomlString: |-
# metadata_dir = "/mnt/meta"
# data_dir = "/mnt/data"
# replication_factor = {{ .Values.garage.replicationFactor }}
# rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
# rpc_secret = "__RPC_SECRET_REPLACE__"
# [kubernetes_discovery]
# namespace = "{{ .Release.Namespace }}"
# service_name = "{{ include "garage.fullname" . }}"
# A rendering-verified version of this example lives in tests/configmap_test.yaml.
garageTomlString: ""
# Data persistence
persistence:
enabled: true
meta:
storageClass: "fast-ssd"
size: 100Mi
# used only for daemon sets
hostPath: /var/lib/garage/meta
data:
storageClass: "standard"
size: 100Mi
# used only for daemon sets
hostPath: /var/lib/garage/data
# Deployment configuration
deployment:
# -- Switchable to DaemonSet
kind: StatefulSet
# -- Number of StatefulSet replicas/garage nodes to start
replicaCount: 3
# -- If using statefulset, allow Parallel or OrderedReady (default)
podManagementPolicy: OrderedReady
image:
# -- default to amd64 docker image
repository: dxflrs/amd64_garage
# -- set the image tag, please prefer using the chart version and not this
# to avoid compatibility issues
tag: ""
pullPolicy: IfNotPresent
initImage:
repository: busybox
tag: stable
pullPolicy: IfNotPresent
# -- set if you need credentials to pull your custom image. Each entry needs a
# `name:` key, matching a Secret of type kubernetes.io/dockerconfigjson.
imagePullSecrets:
- name: my-pull-secret
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Annotations to add to the service account. Example below is for AWS IRSA.
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/garage-s3"
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- additional pod annotations
podAnnotations:
example.com/has-an-annotation: "true"
podSecurityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
runAsNonRoot: true
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
service:
# -- You can rely on any service to expose your cluster
# - ClusterIP (+ Ingress)
# - NodePort (+ Ingress)
# - LoadBalancer
type: ClusterIP
# -- Annotations to add to the service. Example below is for an AWS NLB.
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
s3:
api:
port: 3900
web:
port: 3902
# NOTE: the admin API is excluded for now as it is not consistent across nodes
ingress:
s3:
api:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
labels: {}
hosts:
# -- garage S3 API endpoint, to be used with awscli for example
- host: "s3.garage.tld"
paths:
- path: /
pathType: Prefix
# -- garage S3 API endpoint, DNS style bucket access
- host: "*.s3.garage.tld"
paths:
- path: /
pathType: Prefix
tls:
- secretName: garage-s3-api-tls
hosts:
- s3.garage.tld
- "*.s3.garage.tld"
web:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
labels: {}
hosts:
# -- wildcard website access with bucket name prefix
- host: "*.web.garage.tld"
paths:
- path: /
pathType: Prefix
# -- specific bucket access with FQDN bucket
- host: "mywebpage.example.com"
paths:
- path: /
pathType: Prefix
tls:
- secretName: garage-s3-web-tls
hosts:
- "*.web.garage.tld"
- mywebpage.example.com
# The following are indicative for a small-size deployment, for anything serious double them.
resources:
limits:
cpu: 200m
memory: 2048Mi
requests:
cpu: 100m
memory: 1024Mi
# -- Specifies a livenessProbe
livenessProbe:
httpGet:
path: /health
port: 3903 # or the port from garage.admin.apiBindAddr
initialDelaySeconds: 5
periodSeconds: 30
# -- Specifies a readinessProbe
readinessProbe:
httpGet:
path: /health
port: 3903 # or the port from garage.admin.apiBindAddr
initialDelaySeconds: 5
periodSeconds: 30
# -- Example: pin pods to a dedicated storage node pool, paired with the
# toleration below.
nodeSelector:
node-role.kubernetes.io/storage: "true"
tolerations:
- key: "dedicated"
operator: "Equal"
value: "storage"
effect: "NoSchedule"
# -- Example: spread garage replicas across different nodes, since it is a
# geo-distributed store that only helps availability if replicas don't share
# a failure domain or availability zone.
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: garage
topologyKey: kubernetes.io/hostname
# -- Optional priority class name to assign to the pods.
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
# This is expected to reference a PriorityClass you define yourself.
priorityClassName: "high-priority-storage"
# -- Extra container env vars. Note this is a [] of {name, value} objects (ie. a pod env stanza)
# GARAGE_ADMIN_TOKEN_FILE below points garage at the token file mounted by
# extraVolumes/extraVolumeMounts, see below.
environment:
- name: RUST_LOG
value: "garage=debug"
- name: GARAGE_ADMIN_TOKEN_FILE
value: /mnt/secrets-store/admin-token
# -- Extra volumes/volumeMounts. Both are []. Example here mounts the admin API
# token from an external secrets manager via the Secrets Store CSI driver
# (https://secrets-store-csi-driver.sigs.k8s.io/) instead of a Secret volume.
# This allows, for example, providing the tokens without creating a Kubernetes
# secret. garage reads the mounted file through GARAGE_ADMIN_TOKEN_FILE above.
extraVolumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: garage-admin-token
extraVolumeMounts:
- name: secrets-store
mountPath: /mnt/secrets-store
readOnly: true
monitoring:
metrics:
# -- If true, a service for monitoring is created with a prometheus.io/scrape annotation
enabled: true
serviceMonitor:
# -- If true, a ServiceMonitor CRD is created for a prometheus operator
# https://github.com/coreos/prometheus-operator
enabled: true
path: /metrics
# -- Defaults to the namespace the chart is deployed to; this field is
# templated, so it can also reference .Release.Namespace itself.
namespace: "monitoring"
labels:
release: prometheus
interval: 30s
scheme: http
tlsConfig: {}
scrapeTimeout: 10s
relabelings:
- sourceLabels: ["__meta_kubernetes_pod_node_name"]
targetLabel: node
tracing:
# -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317`
sink: "http://otel-collector.monitoring.svc:4317"
@@ -71,13 +71,6 @@ Create the name of the service account to use
{{- end }} {{- end }}
{{- end }} {{- end }}
{{/*
Extract the trailing port number from a bind address like [::]:3900 or 0.0.0.0:3900.
*/}}
{{- define "garage.portFromBindAddr" -}}
{{- regexFind "[0-9]+$" . -}}
{{- end }}
{{/* {{/*
Returns given number of random Hex characters. Returns given number of random Hex characters.
In practice, it generates up to 100 randAlphaNum strings In practice, it generates up to 100 randAlphaNum strings
@@ -5,11 +5,9 @@ metadata:
labels: labels:
{{- include "garage.labels" . | nindent 4 }} {{- include "garage.labels" . | nindent 4 }}
rules: rules:
{{- if eq .Values.garage.kubernetesSkipCrd false }}
- apiGroups: ["apiextensions.k8s.io"] - apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"] resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "create", "patch"] verbs: ["get", "list", "watch", "create", "patch"]
{{ end }}
- apiGroups: ["deuxfleurs.fr"] - apiGroups: ["deuxfleurs.fr"]
resources: ["garagenodes"] resources: ["garagenodes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+3 -3
View File
@@ -45,16 +45,16 @@ data:
[s3_api] [s3_api]
s3_region = "{{ .Values.garage.s3.api.region }}" s3_region = "{{ .Values.garage.s3.api.region }}"
api_bind_addr = "{{ .Values.garage.s3.api.bindAddr }}" api_bind_addr = "[::]:3900"
root_domain = "{{ .Values.garage.s3.api.rootDomain }}" root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
[s3_web] [s3_web]
bind_addr = "{{ .Values.garage.s3.web.bindAddr }}" bind_addr = "[::]:3902"
root_domain = "{{ .Values.garage.s3.web.rootDomain }}" root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
index = "{{ .Values.garage.s3.web.index }}" index = "{{ .Values.garage.s3.web.index }}"
[admin] [admin]
api_bind_addr = "{{ .Values.garage.admin.apiBindAddr }}" api_bind_addr = "[::]:3903"
{{- if .Values.monitoring.tracing.sink }} {{- if .Values.monitoring.tracing.sink }}
trace_sink = "{{ .Values.monitoring.tracing.sink }}" trace_sink = "{{ .Values.monitoring.tracing.sink }}"
{{- end }} {{- end }}
@@ -62,9 +62,7 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if and .Values.ingress.s3.api.enabled .Values.ingress.s3.web.enabled }}
--- ---
{{ end }}
{{- if .Values.ingress.s3.web.enabled -}} {{- if .Values.ingress.s3.web.enabled -}}
{{- $fullName := include "garage.fullname" . -}} {{- $fullName := include "garage.fullname" . -}}
{{- $svcPort := .Values.service.s3.web.port -}} {{- $svcPort := .Values.service.s3.web.port -}}
@@ -10,11 +10,11 @@ spec:
clusterIP: None clusterIP: None
ports: ports:
- port: {{ .Values.service.s3.api.port }} - port: {{ .Values.service.s3.api.port }}
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }} targetPort: 3900
protocol: TCP protocol: TCP
name: s3-api name: s3-api
- port: {{ .Values.service.s3.web.port }} - port: {{ .Values.service.s3.web.port }}
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }} targetPort: 3902
protocol: TCP protocol: TCP
name: s3-web name: s3-web
selector: selector:
+4 -4
View File
@@ -12,11 +12,11 @@ spec:
type: {{ .Values.service.type }} type: {{ .Values.service.type }}
ports: ports:
- port: {{ .Values.service.s3.api.port }} - port: {{ .Values.service.s3.api.port }}
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }} targetPort: 3900
protocol: TCP protocol: TCP
name: s3-api name: s3-api
- port: {{ .Values.service.s3.web.port }} - port: {{ .Values.service.s3.web.port }}
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }} targetPort: 3902
protocol: TCP protocol: TCP
name: s3-web name: s3-web
selector: selector:
@@ -35,8 +35,8 @@ spec:
type: ClusterIP type: ClusterIP
clusterIP: None clusterIP: None
ports: ports:
- port: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }} - port: 3903
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }} targetPort: 3903
protocol: TCP protocol: TCP
name: metrics name: metrics
selector: selector:
@@ -28,11 +28,11 @@ spec:
scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }} scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }}
{{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }} {{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }}
tlsConfig: tlsConfig:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 6 }}
{{- end }} {{- end }}
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }} {{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
relabelings: relabelings:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 6 }}
{{- end }} {{- end }}
jobLabel: "{{ .Release.Name }}" jobLabel: "{{ .Release.Name }}"
selector: selector:
+4 -7
View File
@@ -28,9 +28,6 @@ spec:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
serviceAccountName: {{ include "garage.serviceAccountName" . }} serviceAccountName: {{ include "garage.serviceAccountName" . }}
{{- with .Values.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
securityContext: securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }} {{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers: initContainers:
@@ -60,11 +57,11 @@ spec:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }} - containerPort: 3900
name: s3-api name: s3-api
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }} - containerPort: 3902
name: web-api name: web-api
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }} - containerPort: 3903
name: admin name: admin
{{- with .Values.environment }} {{- with .Values.environment }}
env: env:
@@ -94,7 +91,7 @@ spec:
volumes: volumes:
- name: configmap - name: configmap
configMap: configMap:
name: {{ if .Values.garage.existingConfigMap }}{{ .Values.garage.existingConfigMap }}{{ else }}{{ include "garage.fullname" . }}-config{{ end }} name: {{ include "garage.fullname" . }}-config
- name: etc - name: etc
emptyDir: {} emptyDir: {}
{{- if .Values.persistence.enabled }} {{- if .Values.persistence.enabled }}
@@ -1,31 +0,0 @@
suite: rbac
templates:
- templates/clusterrole.yaml
tests:
- it: allows managing the garage CRD by default
asserts:
- hasDocuments:
count: 2
- documentIndex: 0
isKind:
of: ClusterRole
- documentIndex: 0
contains:
path: rules[0].resources
content: customresourcedefinitions
- documentIndex: 1
isKind:
of: ClusterRoleBinding
- documentIndex: 1
equal:
path: subjects[0].name
value: RELEASE-NAME-garage
- it: skips the CRD management rule when the CRD is installed manually
set:
garage.kubernetesSkipCrd: true
asserts:
- documentIndex: 0
notContains:
path: rules[0].resources
content: customresourcedefinitions
@@ -1,82 +0,0 @@
# Integration-style suite: renders the whole chart with complex-values.yaml
# (many non-default features combined at once) and checks that they don't
# clobber each other, rather than testing any single feature in isolation
# (that's what the other tests/*_test.yaml suites are for).
suite: complex-values integration
templates:
- templates/workload.yaml
- templates/service.yaml
- templates/service-headless.yaml
- templates/ingress.yaml
- templates/servicemonitor.yaml
- templates/serviceaccount.yaml
- templates/configmap.yaml
- templates/clusterrole.yaml
tests:
- it: renders a self-consistent deployment with every optional feature enabled
values:
- ../complex-values.yaml
asserts:
- template: templates/workload.yaml
isKind:
of: StatefulSet
- template: templates/workload.yaml
equal:
path: metadata.labels.team
value: platform-infrastructure
- template: templates/workload.yaml
equal:
path: spec.template.spec.containers[0].env[0].name
value: RUST_LOG
- template: templates/workload.yaml
contains:
path: spec.template.spec.volumes
content:
name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: garage-admin-token
- template: templates/workload.yaml
equal:
path: spec.template.spec.containers[0].env[1].name
value: GARAGE_ADMIN_TOKEN_FILE
- template: templates/workload.yaml
equal:
path: spec.volumeClaimTemplates[0].spec.storageClassName
value: fast-ssd
- template: templates/workload.yaml
contains:
path: spec.template.spec.imagePullSecrets
content:
name: my-pull-secret
- template: templates/service.yaml
hasDocuments:
count: 2 # main service + metrics service, since monitoring.metrics.enabled is true here
- template: templates/service-headless.yaml
hasDocuments:
count: 1 # StatefulSet still gets a headless service
- template: templates/ingress.yaml
hasDocuments:
count: 2 # both s3 api and s3 web ingresses enabled together
- template: templates/servicemonitor.yaml
hasDocuments:
count: 1
- template: templates/servicemonitor.yaml
equal:
path: spec.endpoints[0].relabelings[0].targetLabel
value: node
- template: templates/serviceaccount.yaml
equal:
path: metadata.annotations["eks.amazonaws.com/role-arn"]
value: "arn:aws:iam::123456789012:role/garage-s3"
- template: templates/configmap.yaml
matchRegex:
path: data["garage.toml"]
pattern: 'data_fsync = true'
- template: templates/clusterrole.yaml
documentIndex: 0
notContains:
path: rules[0].resources
content: customresourcedefinitions # garage.kubernetesSkipCrd is true here
@@ -1,136 +0,0 @@
suite: configmap
templates:
- templates/configmap.yaml
tests:
- it: renders garage.toml with the default configuration
asserts:
- hasDocuments:
count: 1
- isKind:
of: ConfigMap
- equal:
path: metadata.name
value: RELEASE-NAME-garage-config
- matchRegex:
path: data["garage.toml"]
pattern: 'metadata_dir = "/mnt/meta"'
- matchRegex:
path: data["garage.toml"]
pattern: 'data_dir = "/mnt/data"'
- matchRegex:
path: data["garage.toml"]
pattern: 'db_engine = "lmdb"'
- matchRegex:
path: data["garage.toml"]
pattern: 'block_size = "1048576"'
- matchRegex:
path: data["garage.toml"]
pattern: 'replication_factor = 3'
- matchRegex:
path: data["garage.toml"]
pattern: 'consistency_mode = "consistent"'
- matchRegex:
path: data["garage.toml"]
pattern: 'compression_level = 1'
- matchRegex:
path: data["garage.toml"]
pattern: 'rpc_bind_addr = "\[::\]:3901"'
- matchRegex:
path: data["garage.toml"]
pattern: 'rpc_secret = "__RPC_SECRET_REPLACE__"'
- matchRegex:
path: data["garage.toml"]
pattern: '(?s)\[kubernetes_discovery\]\s*namespace = "NAMESPACE"\s*service_name = "RELEASE-NAME-garage"\s*skip_crd = false'
- matchRegex:
path: data["garage.toml"]
pattern: '(?s)\[s3_api\]\s*s3_region = "garage"\s*api_bind_addr = "\[::\]:3900"\s*root_domain = "\.s3\.garage\.tld"'
- matchRegex:
path: data["garage.toml"]
pattern: '(?s)\[s3_web\]\s*bind_addr = "\[::\]:3902"\s*root_domain = "\.web\.garage\.tld"\s*index = "index.html"'
- matchRegex:
path: data["garage.toml"]
pattern: '(?s)\[admin\]\s*api_bind_addr = "\[::\]:3903"'
- notMatchRegex:
path: data["garage.toml"]
pattern: 'metadata_auto_snapshot_interval'
- notMatchRegex:
path: data["garage.toml"]
pattern: 'trace_sink'
- it: reflects custom garage settings, bootstrap peers and additional config
set:
garage.dbEngine: sqlite
garage.blockSize: "2097152"
garage.replicationFactor: "5"
garage.consistencyMode: degraded
garage.compressionLevel: "3"
garage.metadataAutoSnapshotInterval: 6h
garage.bootstrapPeers:
- abc@peer1:3901
- def@peer2:3901
garage.additionalTopLevelConfig: "data_fsync = true"
monitoring.tracing.sink: http://otel:4317
asserts:
- matchRegex:
path: data["garage.toml"]
pattern: 'db_engine = "sqlite"'
- matchRegex:
path: data["garage.toml"]
pattern: 'block_size = "2097152"'
- matchRegex:
path: data["garage.toml"]
pattern: 'replication_factor = 5'
- matchRegex:
path: data["garage.toml"]
pattern: 'consistency_mode = "degraded"'
- matchRegex:
path: data["garage.toml"]
pattern: 'compression_level = 3'
- matchRegex:
path: data["garage.toml"]
pattern: 'metadata_auto_snapshot_interval = "6h"'
- matchRegex:
path: data["garage.toml"]
pattern: 'bootstrap_peers = \["abc@peer1:3901"\s*, "def@peer2:3901"'
- matchRegex:
path: data["garage.toml"]
pattern: 'data_fsync = true'
- matchRegex:
path: data["garage.toml"]
pattern: 'trace_sink = "http://otel:4317"'
- it: uses garageTomlString verbatim when set, ignoring the structured values
set:
garage.garageTomlString: |-
metadata_dir = "/custom/meta"
replication_factor = 1
garage.dbEngine: sqlite
asserts:
- equal:
path: data["garage.toml"]
value: |-
metadata_dir = "/custom/meta"
replication_factor = 1
- notMatchRegex:
path: data["garage.toml"]
pattern: 'db_engine'
- it: templates garageTomlString against the release and values context
set:
garage.garageTomlString: |-
# namespace: {{ .Release.Namespace }}
replication_factor = {{ .Values.garage.replicationFactor }}
garage.replicationFactor: "7"
asserts:
- equal:
path: data["garage.toml"]
value: |-
# namespace: NAMESPACE
replication_factor = 7
- it: does not render a ConfigMap when an existing one is referenced
set:
garage.existingConfigMap: my-external-cm
asserts:
- hasDocuments:
count: 0
@@ -1,95 +0,0 @@
suite: ingress
templates:
- templates/ingress.yaml
tests:
- it: renders no ingress by default
asserts:
- hasDocuments:
count: 0
- it: renders api and web ingresses with tls when enabled
values:
- ./values/ingress.yaml
asserts:
- hasDocuments:
count: 2
- isKind:
of: Ingress
- documentIndex: 0
equal:
path: metadata.name
value: RELEASE-NAME-garage-s3-api
- documentIndex: 0
equal:
path: spec.ingressClassName
value: nginx
- documentIndex: 0
equal:
path: spec.rules[0].host
value: s3.example.com
- documentIndex: 0
equal:
path: spec.tls[0].secretName
value: garage-s3-api-tls
- documentIndex: 1
equal:
path: metadata.name
value: RELEASE-NAME-garage-s3-web
- documentIndex: 1
equal:
path: spec.rules[0].host
value: "*.web.example.com"
- documentIndex: 1
equal:
path: spec.tls[0].secretName
value: garage-s3-web-tls
- it: can enable only the s3 api ingress
set:
ingress.s3.api.enabled: true
ingress.s3.api.hosts[0].host: s3.example.com
ingress.s3.api.hosts[0].paths[0].path: /
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
asserts:
- hasDocuments:
count: 1
- equal:
path: metadata.name
value: RELEASE-NAME-garage-s3-api
- it: omits ingressClassName and tls when neither is configured
set:
ingress.s3.api.enabled: true
ingress.s3.api.hosts[0].host: s3.example.com
ingress.s3.api.hosts[0].paths[0].path: /
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
asserts:
- isNull:
path: spec.ingressClassName
- isNull:
path: spec.tls
- it: renders multiple hosts on the same ingress
set:
ingress.s3.api.enabled: true
ingress.s3.api.hosts:
- host: s3.example.com
paths:
- path: /
pathType: Prefix
- host: s3-alt.example.com
paths:
- path: /
pathType: Prefix
asserts:
- hasDocuments:
count: 1
- lengthEqual:
path: spec.rules
count: 2
- equal:
path: spec.rules[0].host
value: s3.example.com
- equal:
path: spec.rules[1].host
value: s3-alt.example.com
-56
View File
@@ -1,56 +0,0 @@
suite: naming and common labels
templates:
- templates/workload.yaml
- templates/configmap.yaml
tests:
- it: applies commonLabels alongside the default chart labels
template: templates/workload.yaml
set:
commonLabels:
team: storage
asserts:
- equal:
path: metadata.labels.team
value: storage
- equal:
path: metadata.labels["app.kubernetes.io/managed-by"]
value: Helm
- it: uses fullnameOverride verbatim for resource names
template: templates/workload.yaml
set:
fullnameOverride: my-garage-cluster
asserts:
- equal:
path: metadata.name
value: my-garage-cluster
- equal:
path: spec.serviceName
value: my-garage-cluster-headless
- it: does not double-prefix when the release name already contains the chart name
template: templates/workload.yaml
release:
name: garage
asserts:
- equal:
path: metadata.name
value: garage
- it: prefixes the release name with the chart name otherwise
template: templates/workload.yaml
release:
name: prod
asserts:
- equal:
path: metadata.name
value: prod-garage
- it: truncates an overly long fullname to 63 characters and trims a trailing dash
template: templates/workload.yaml
set:
fullnameOverride: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-suffix-that-will-be-cut-off
asserts:
- equal:
path: metadata.name
value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-33
View File
@@ -1,33 +0,0 @@
suite: rpc secret
templates:
- templates/secret.yaml
tests:
- it: generates a Secret holding the rpc secret by default
asserts:
- hasDocuments:
count: 1
- isKind:
of: Secret
- equal:
path: metadata.name
value: RELEASE-NAME-garage-rpc-secret
- equal:
path: type
value: Opaque
- isNotNull:
path: data.rpcSecret
- it: does not render a Secret when an existing one is referenced
values:
- ./values/existing-secret.yaml
asserts:
- hasDocuments:
count: 0
- it: base64-encodes an explicitly provided rpc secret
set:
garage.rpcSecret: my-plain-secret
asserts:
- equal:
path: data.rpcSecret
value: bXktcGxhaW4tc2VjcmV0
@@ -1,26 +0,0 @@
suite: headless service
templates:
- templates/service-headless.yaml
tests:
- it: creates a headless service for a StatefulSet by default
asserts:
- hasDocuments:
count: 1
- isKind:
of: Service
- equal:
path: metadata.name
value: RELEASE-NAME-garage-headless
- equal:
path: spec.clusterIP
value: None
- equal:
path: spec.type
value: ClusterIP
- it: does not create a headless service for a DaemonSet
values:
- ./values/daemonset.yaml
asserts:
- hasDocuments:
count: 0
@@ -1,61 +0,0 @@
suite: service
templates:
- templates/service.yaml
tests:
- it: creates a ClusterIP service with s3-api and s3-web ports by default
asserts:
- hasDocuments:
count: 1
- isKind:
of: Service
- equal:
path: spec.type
value: ClusterIP
- equal:
path: spec.ports[0].name
value: s3-api
- equal:
path: spec.ports[0].port
value: 3900
- equal:
path: spec.ports[1].name
value: s3-web
- equal:
path: spec.ports[1].port
value: 3902
- it: honors a custom service type and port
set:
service.type: LoadBalancer
service.s3.api.port: 9000
asserts:
- equal:
path: spec.type
value: LoadBalancer
- equal:
path: spec.ports[0].port
value: 9000
- it: does not create a metrics service by default
asserts:
- hasDocuments:
count: 1
- it: adds a headless metrics service when monitoring is enabled
values:
- ./values/monitoring.yaml
asserts:
- hasDocuments:
count: 2
- documentIndex: 1
equal:
path: metadata.name
value: RELEASE-NAME-garage-metrics
- documentIndex: 1
equal:
path: spec.clusterIP
value: None
- documentIndex: 1
equal:
path: metadata.annotations["prometheus.io/scrape"]
value: "true"
@@ -1,28 +0,0 @@
suite: service account
templates:
- templates/serviceaccount.yaml
tests:
- it: creates a ServiceAccount by default
asserts:
- hasDocuments:
count: 1
- isKind:
of: ServiceAccount
- equal:
path: metadata.name
value: RELEASE-NAME-garage
- it: does not create a ServiceAccount when disabled
values:
- ./values/minimal.yaml
asserts:
- hasDocuments:
count: 0
- it: honors a custom service account name
set:
serviceAccount.name: my-garage-sa
asserts:
- equal:
path: metadata.name
value: my-garage-sa
@@ -1,65 +0,0 @@
suite: service monitor
templates:
- templates/servicemonitor.yaml
tests:
- it: renders no ServiceMonitor by default
asserts:
- hasDocuments:
count: 0
- it: renders no ServiceMonitor when only metrics are enabled
set:
monitoring.metrics.enabled: true
asserts:
- hasDocuments:
count: 0
- it: renders a ServiceMonitor when explicitly enabled
values:
- ./values/monitoring.yaml
asserts:
- hasDocuments:
count: 1
- isKind:
of: ServiceMonitor
- equal:
path: metadata.name
value: RELEASE-NAME-garage
- equal:
path: metadata.namespace
value: NAMESPACE
- equal:
path: spec.endpoints[0].interval
value: 30s
- it: templates a custom namespace against the release context
values:
- ./values/monitoring.yaml
set:
monitoring.metrics.serviceMonitor.namespace: "{{ .Release.Namespace }}-monitoring"
asserts:
- equal:
path: metadata.namespace
value: NAMESPACE-monitoring
- it: applies custom labels, tlsConfig and relabelings
values:
- ./values/monitoring.yaml
set:
monitoring.metrics.serviceMonitor.labels:
team: storage
monitoring.metrics.serviceMonitor.tlsConfig:
insecureSkipVerify: true
monitoring.metrics.serviceMonitor.relabelings:
- sourceLabels: ["__meta_kubernetes_pod_name"]
targetLabel: pod
asserts:
- equal:
path: metadata.labels.team
value: storage
- equal:
path: spec.endpoints[0].tlsConfig.insecureSkipVerify
value: true
- equal:
path: spec.endpoints[0].relabelings[0].targetLabel
value: pod
@@ -1,10 +0,0 @@
# Run garage as a DaemonSet (one pod per node) instead of the default StatefulSet,
# using hostPath volumes for meta/data persistence.
deployment:
kind: DaemonSet
persistence:
enabled: true
meta:
hostPath: /var/lib/garage/meta
data:
hostPath: /var/lib/garage/data
@@ -1,5 +0,0 @@
# Use a pre-existing Kubernetes Secret for the RPC secret instead of letting
# the chart generate/manage one.
garage:
rpcSecret: ""
existingRpcSecret: "garage-rpc-secret-external"
@@ -1,27 +0,0 @@
# Expose both the S3 API and website endpoints through Ingress, with TLS.
ingress:
s3:
api:
enabled: true
className: "nginx"
hosts:
- host: "s3.example.com"
paths:
- path: /
pathType: Prefix
tls:
- secretName: garage-s3-api-tls
hosts:
- s3.example.com
web:
enabled: true
className: "nginx"
hosts:
- host: "*.web.example.com"
paths:
- path: /
pathType: Prefix
tls:
- secretName: garage-s3-web-tls
hosts:
- "*.web.example.com"
@@ -1,8 +0,0 @@
# Minimal single-node deployment without persistent storage or a dedicated
# service account, e.g. for local testing.
deployment:
replicaCount: 1
persistence:
enabled: false
serviceAccount:
create: false
@@ -1,7 +0,0 @@
# Enable Prometheus metrics scraping and a ServiceMonitor for the prometheus-operator.
monitoring:
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
-187
View File
@@ -1,187 +0,0 @@
suite: workload (StatefulSet/DaemonSet)
templates:
- templates/workload.yaml
- templates/configmap.yaml
tests:
- it: defaults to a StatefulSet with 3 replicas and 2 volumes
template: templates/workload.yaml
asserts:
- isKind:
of: StatefulSet
- equal:
path: spec.replicas
value: 3
- equal:
path: spec.podManagementPolicy
value: OrderedReady
- equal:
path: spec.template.spec.volumes[1].name
value: etc
- lengthEqual:
path: spec.template.spec.volumes
count: 2
- isNotNull:
path: spec.volumeClaimTemplates
- it: uses a StatefulSet with PVC-backed volumeClaimTemplates by default
template: templates/workload.yaml
asserts:
- isKind:
of: StatefulSet
- isNotNull:
path: spec.volumeClaimTemplates
- equal:
path: spec.volumeClaimTemplates[0].spec.resources.requests.storage
value: 100Mi
- it: switches to a DaemonSet with hostPath volumes when requested
template: templates/workload.yaml
values:
- ./values/daemonset.yaml
asserts:
- isKind:
of: DaemonSet
- isNull:
path: spec.replicas
- isNull:
path: spec.volumeClaimTemplates
- contains:
path: spec.template.spec.volumes
content:
name: meta
hostPath:
path: /var/lib/garage/meta
type: DirectoryOrCreate
- contains:
path: spec.template.spec.volumes
content:
name: data
hostPath:
path: /var/lib/garage/data
type: DirectoryOrCreate
- it: renders emptyDir volumes when persistence is disabled
template: templates/workload.yaml
values:
- ./values/minimal.yaml
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: meta
emptyDir: {}
- contains:
path: spec.template.spec.volumes
content:
name: data
emptyDir: {}
- isNull:
path: spec.volumeClaimTemplates
- it: honors a custom replicaCount
template: templates/workload.yaml
set:
deployment.replicaCount: 5
asserts:
- equal:
path: spec.replicas
value: 5
- it: points the init container at the configured rpc secret
template: templates/workload.yaml
asserts:
- equal:
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
value: RELEASE-NAME-garage-rpc-secret
- it: points the init container at an existing rpc secret when configured
template: templates/workload.yaml
values:
- ./values/existing-secret.yaml
asserts:
- equal:
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
value: garage-rpc-secret-external
- it: sets the container image from repository and tag
template: templates/workload.yaml
set:
image.repository: dxflrs/amd64_garage
image.tag: v1.2.3
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: dxflrs/amd64_garage:v1.2.3
- it: falls back to the chart appVersion when no image tag is set
template: templates/workload.yaml
asserts:
- matchRegex:
path: spec.template.spec.containers[0].image
pattern: ^dxflrs/amd64_garage:v
- it: omits storageClassName from volumeClaimTemplates by default
template: templates/workload.yaml
asserts:
- isNull:
path: spec.volumeClaimTemplates[0].spec.storageClassName
- isNull:
path: spec.volumeClaimTemplates[1].spec.storageClassName
- it: sets storageClassName in volumeClaimTemplates when configured
template: templates/workload.yaml
set:
persistence.meta.storageClass: fast-storage
persistence.data.storageClass: slow-storage
asserts:
- equal:
path: spec.volumeClaimTemplates[0].spec.storageClassName
value: fast-storage
- equal:
path: spec.volumeClaimTemplates[1].spec.storageClassName
value: slow-storage
- it: renders emptyDir volumes for a DaemonSet when persistence is disabled
template: templates/workload.yaml
set:
deployment.kind: DaemonSet
persistence.enabled: false
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: meta
emptyDir: {}
- contains:
path: spec.template.spec.volumes
content:
name: data
emptyDir: {}
- it: mounts the existing ConfigMap volume when configured
template: templates/workload.yaml
set:
garage.existingConfigMap: my-external-cm
asserts:
- equal:
path: spec.template.spec.volumes[0].configMap.name
value: my-external-cm
- it: uses a custom service account name without creating one when disabled
template: templates/workload.yaml
set:
serviceAccount.create: false
serviceAccount.name: my-external-sa
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: my-external-sa
- it: falls back to the default service account when disabled without a custom name
template: templates/workload.yaml
set:
serviceAccount.create: false
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: default
+2 -14
View File
@@ -48,15 +48,11 @@ garage:
kubernetesSkipCrd: false kubernetesSkipCrd: false
s3: s3:
api: api:
bindAddr: "[::]:3900"
region: "garage" region: "garage"
rootDomain: ".s3.garage.tld" rootDomain: ".s3.garage.tld"
web: web:
bindAddr: "[::]:3902"
rootDomain: ".web.garage.tld" rootDomain: ".web.garage.tld"
index: "index.html" index: "index.html"
admin:
apiBindAddr: "[::]:3903"
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config. # -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
# Example: # Example:
@@ -225,14 +221,14 @@ resources: {}
livenessProbe: {} livenessProbe: {}
#httpGet: #httpGet:
# path: /health # path: /health
# port: 3903 # or the port from garage.admin.apiBindAddr # port: 3903
#initialDelaySeconds: 5 #initialDelaySeconds: 5
#periodSeconds: 30 #periodSeconds: 30
# -- Specifies a readinessProbe # -- Specifies a readinessProbe
readinessProbe: {} readinessProbe: {}
#httpGet: #httpGet:
# path: /health # path: /health
# port: 3903 # or the port from garage.admin.apiBindAddr # port: 3903
#initialDelaySeconds: 5 #initialDelaySeconds: 5
#periodSeconds: 30 #periodSeconds: 30
@@ -242,18 +238,10 @@ tolerations: []
affinity: {} affinity: {}
# -- Optional priority class name to assign to the pods.
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
priorityClassName: ""
# -- Extra container env vars, as a list of {name, value} objects (same shape
# as a Pod container's env)
environment: {} environment: {}
# -- Extra volumes, as a list of volume objects (same shape as a PodSpec's volumes)
extraVolumes: {} extraVolumes: {}
# -- Extra volume mounts, as a list of mount objects (same shape as a container's volumeMounts)
extraVolumeMounts: {} extraVolumeMounts: {}
monitoring: monitoring:
+1 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_admin" name = "garage_api_admin"
version = "2.3.0" version = "2.2.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -48,6 +48,3 @@ prometheus = { workspace = true, optional = true }
[features] [features]
metrics = ["opentelemetry-prometheus", "prometheus"] metrics = ["opentelemetry-prometheus", "prometheus"]
k2v = ["garage_model/k2v"] k2v = ["garage_model/k2v"]
[lints]
workspace = true
+4 -5
View File
@@ -7,7 +7,6 @@ use garage_util::time::now_msec;
use garage_model::admin_token_table::*; use garage_model::admin_token_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::permission::ExpirationTime;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -245,8 +244,8 @@ fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInf
.expect("invalid timestamp stored in db"), .expect("invalid timestamp stored in db"),
), ),
name: params.name.get().to_string(), name: params.name.get().to_string(),
expiration: params.expiration.get().inner().map(|x| { expiration: params.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}), }),
expired: params.is_expired(now), expired: params.is_expired(now),
scope: params.scope.get().0.clone(), scope: params.scope.get().0.clone(),
@@ -280,10 +279,10 @@ fn apply_token_updates(
if let Some(expiration) = updates.expiration { if let Some(expiration) = updates.expiration {
params params
.expiration .expiration
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into()); .update(Some(expiration.timestamp_millis() as u64));
} }
if updates.never_expires { if updates.never_expires {
params.expiration.update(None.into()); params.expiration.update(None);
} }
if let Some(scope) = updates.scope { if let Some(scope) = updates.scope {
params.scope.update(AdminApiTokenScope(scope)); params.scope.update(AdminApiTokenScope(scope));
+2 -124
View File
@@ -12,7 +12,7 @@ use garage_rpc::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_api_common::{common_error::CommonError, helpers::is_default, xml}; use garage_api_common::{common_error::CommonError, helpers::is_default};
use crate::api_server::{find_matching_nodes, AdminRpc, AdminRpcResponse}; use crate::api_server::{find_matching_nodes, AdminRpc, AdminRpcResponse};
use crate::error::Error; use crate::error::Error;
@@ -282,34 +282,8 @@ pub struct GetClusterHealthResponse {
pub struct GetClusterStatisticsRequest; pub struct GetClusterStatisticsRequest;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetClusterStatisticsResponse { pub struct GetClusterStatisticsResponse {
// FIXME for v3: remove freeform field and move display logic to garage crate
/// cluster statistics as a free-form string, kept for compatibility with nodes
/// running older v2.x versions of garage
pub freeform: String, pub freeform: String,
// FIXME for v3: remove Option<> and serde(default) for all fields below
/// available storage space for object data in the entire cluster, in bytes
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_avail: Option<u64>,
/// available storage space for object metadata in the entire cluster, in bytes
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata_avail: Option<u64>,
/// true if the available storage space statistics are imprecise due to missing
/// information of disconnected nodes. When this is the case, the actual
/// space available in the cluster might be lower than the reported values.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub incomplete_avail_info: Option<bool>,
/// number of buckets in the cluster
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bucket_count: Option<u64>,
/// total number of objects stored in all buckets
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_object_count: Option<u64>,
/// total size of objects stored in all buckets, before compression, deduplication and
/// replication (this is NOT equivalent to actual disk usage in the cluster)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_object_bytes: Option<u64>,
} }
// ---- ConnectClusterNodes ---- // ---- ConnectClusterNodes ----
@@ -618,10 +592,6 @@ pub enum PreviewClusterLayoutChangesResponse {
/// Plain-text information about the layout computation /// Plain-text information about the layout computation
/// (do not try to parse this) /// (do not try to parse this)
message: Vec<String>, message: Vec<String>,
/// Structured statistics about the layout computation
// FIXME for v3: remove default and skip_serializing_if
#[serde(default, skip_serializing_if = "Option::is_none")]
statistics: Option<Box<garage_rpc::layout::ComputationStat>>,
/// Details about the new cluster layout /// Details about the new cluster layout
new_layout: GetClusterLayoutResponse, new_layout: GetClusterLayoutResponse,
}, },
@@ -643,10 +613,6 @@ pub struct ApplyClusterLayoutResponse {
/// Plain-text information about the layout computation /// Plain-text information about the layout computation
/// (do not try to parse this) /// (do not try to parse this)
pub message: Vec<String>, pub message: Vec<String>,
/// Structured statistics about the layout computation
// FIXME for v3: remove default and skip_serializing_if
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statistics: Option<garage_rpc::layout::ComputationStat>,
/// Details about the new cluster layout /// Details about the new cluster layout
pub layout: GetClusterLayoutResponse, pub layout: GetClusterLayoutResponse,
} }
@@ -877,16 +843,9 @@ pub struct GetBucketInfoResponse {
pub global_aliases: Vec<String>, pub global_aliases: Vec<String>,
/// Whether website access is enabled for this bucket /// Whether website access is enabled for this bucket
pub website_access: bool, pub website_access: bool,
#[serde(default)]
/// Website configuration for this bucket /// Website configuration for this bucket
#[serde(default, skip_serializing_if = "Option::is_none")]
pub website_config: Option<GetBucketInfoWebsiteResponse>, pub website_config: Option<GetBucketInfoWebsiteResponse>,
// FIXME for v3: remove serde(default) for the two fields below
/// CORS rules for this bucket
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cors_rules: Option<Vec<xml::cors::CorsRule>>,
/// Object lifecycle rules for this bucket
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifecycle_rules: Option<Vec<xml::lifecycle::LifecycleRule>>,
/// List of access keys that have permissions granted on this bucket /// List of access keys that have permissions granted on this bucket
pub keys: Vec<GetBucketInfoKey>, pub keys: Vec<GetBucketInfoKey>,
/// Number of objects in this bucket /// Number of objects in this bucket
@@ -910,9 +869,6 @@ pub struct GetBucketInfoResponse {
pub struct GetBucketInfoWebsiteResponse { pub struct GetBucketInfoWebsiteResponse {
pub index_document: String, pub index_document: String,
pub error_document: Option<String>, pub error_document: Option<String>,
// FIXME for v3: remove serde(default) for field below
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing_rules: Option<Vec<xml::website::RoutingRule>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -971,11 +927,6 @@ pub struct UpdateBucketResponse(pub GetBucketInfoResponse);
pub struct UpdateBucketRequestBody { pub struct UpdateBucketRequestBody {
pub website_access: Option<UpdateBucketWebsiteAccess>, pub website_access: Option<UpdateBucketWebsiteAccess>,
pub quotas: Option<ApiBucketQuotas>, pub quotas: Option<ApiBucketQuotas>,
// FIXME for v3: remove serde(default) for the two fields below
#[serde(default)]
pub cors_rules: Option<Vec<xml::cors::CorsRule>>,
#[serde(default)]
pub lifecycle_rules: Option<Vec<xml::lifecycle::LifecycleRule>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -984,9 +935,6 @@ pub struct UpdateBucketWebsiteAccess {
pub enabled: bool, pub enabled: bool,
pub index_document: Option<String>, pub index_document: Option<String>,
pub error_document: Option<String>, pub error_document: Option<String>,
// FIXME for v3: remove serde(default) for field below
#[serde(default)]
pub routing_rules: Option<Vec<xml::website::RoutingRule>>,
} }
// ---- DeleteBucket ---- // ---- DeleteBucket ----
@@ -1161,41 +1109,10 @@ pub struct LocalGetNodeInfoRequest;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LocalGetNodeInfoResponse { pub struct LocalGetNodeInfoResponse {
pub node_id: String, pub node_id: String,
// FIXME for v3: remove Option<> and serde(default) for field below
/// hostname of this node
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
/// garage version running on this node
pub garage_version: String, pub garage_version: String,
/// build-time features enabled for this garage release
pub garage_features: Option<Vec<String>>, pub garage_features: Option<Vec<String>>,
/// rustc version with which this garage release was compiled
pub rust_version: String, pub rust_version: String,
/// database engine used for metadata
pub db_engine: String, pub db_engine: String,
// FIXME for v3: remove Option<> and serde(default) for field below
// FIXME for v3: merge LocalGetNodeInfoResponse and NodeResp
/// Socket address used by other nodes to connect to this node for RPC
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<String>)]
pub addr: Option<SocketAddr>,
/// Whether this node is connected in the cluster
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_up: Option<bool>,
/// Role assigned to this node in the current cluster layout
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<NodeAssignedRole>,
/// Whether this node is part of an older layout version and is draining data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub draining: Option<bool>,
/// Total and available space on the disk partition(s) containing the data
/// directory(ies)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_partition: Option<FreeSpaceResp>,
/// Total and available space on the disk partition containing the
/// metadata directory
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata_partition: Option<FreeSpaceResp>,
} }
// ---- GetNodeStatistics ---- // ---- GetNodeStatistics ----
@@ -1204,47 +1121,8 @@ pub struct LocalGetNodeInfoResponse {
pub struct LocalGetNodeStatisticsRequest; pub struct LocalGetNodeStatisticsRequest;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LocalGetNodeStatisticsResponse { pub struct LocalGetNodeStatisticsResponse {
// FIXME for v3: remove freeform field and move display logic to garage crate
/// node statistics as a free-form string, kept for compatibility with nodes
/// running older v2.x versions of garage
pub freeform: String, pub freeform: String,
// FIXME for v3: remove serde(default) for fields below
/// metadata table statistics
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_stats: Option<Vec<NodeTableStats>>,
/// block manager statistics
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_manager_stats: Option<NodeBlockManagerStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct NodeTableStats {
/// name of metadata table
pub table_name: String,
/// number of items stored in metadata table
pub items: u64,
/// size of the merkle tree representing all items in the table
pub merkle_items: u64,
/// number of items in the merkle tree update queue
pub merkle_queue_len: u64,
/// number of items in the remote insert queue
pub insert_queue_len: u64,
/// number of items in the garbage collection queue
pub gc_queue_len: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct NodeBlockManagerStats {
/// number of reference counter entries
pub rc_entries: u64,
/// number of blocks in the resync queue
pub resync_queue_len: u64,
/// number of blocks with resync errors
pub resync_errors: u64,
} }
// ---- CreateMetadataSnapshot ---- // ---- CreateMetadataSnapshot ----
+57 -120
View File
@@ -18,7 +18,6 @@ use garage_model::s3::mpu_table;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_api_common::common_error::CommonError; use garage_api_common::common_error::CommonError;
use garage_api_common::xml;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -38,7 +37,7 @@ impl RequestHandler for ListBucketsRequest {
&EmptyKey, &EmptyKey,
None, None,
Some(DeletedFilter::NotDeleted), Some(DeletedFilter::NotDeleted),
1_000_000, 10000,
EnumerationOrder::Forward, EnumerationOrder::Forward,
) )
.await?; .await?;
@@ -90,7 +89,7 @@ impl RequestHandler for GetBucketInfoRequest {
.bucket_alias_table .bucket_alias_table
.get(&EmptyKey, &ga) .get(&EmptyKey, &ga)
.await? .await?
.and_then(|x| x.state.get().into_inner()) .and_then(|x| *x.state.get())
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?, .ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
(None, None, Some(search)) => { (None, None, Some(search)) => {
let helper = garage.bucket_helper(); let helper = garage.bucket_helper();
@@ -168,7 +167,7 @@ impl RequestHandler for CreateBucketRequest {
} }
if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? { if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? {
if alias.state.get().inner().is_some() { if alias.state.get().is_some() {
return Err(CommonError::BucketAlreadyExists.into()); return Err(CommonError::BucketAlreadyExists.into());
} }
} }
@@ -294,46 +293,25 @@ impl RequestHandler for UpdateBucketRequest {
if let Some(wa) = self.body.website_access { if let Some(wa) = self.body.website_access {
if wa.enabled { if wa.enabled {
let redirect_all = state let (redirect_all, routing_rules) = match state.website_config.get() {
.website_config Some(wc) => (wc.redirect_all.clone(), wc.routing_rules.clone()),
.get() None => (None, Vec::new()),
.inner()
.and_then(|wc| wc.redirect_all.clone());
let routing_rules = if let Some(rr) = wa.routing_rules {
for r in rr.iter() {
r.validate()?;
}
rr.into_iter()
.map(xml::website::RoutingRule::into_garage_routing_rule)
.collect::<Vec<_>>()
} else {
state
.website_config
.get()
.inner()
.map(|wc| wc.routing_rules.clone())
.unwrap_or_default()
}; };
state.website_config.update(Some(WebsiteConfig {
state.website_config.update(
Some(WebsiteConfig {
index_document: wa.index_document.ok_or_bad_request( index_document: wa.index_document.ok_or_bad_request(
"Please specify indexDocument when enabling website access.", "Please specify indexDocument when enabling website access.",
)?, )?,
error_document: wa.error_document, error_document: wa.error_document,
redirect_all, redirect_all,
routing_rules, routing_rules,
}) }));
.into(),
);
} else { } else {
if wa.index_document.is_some() || wa.error_document.is_some() { if wa.index_document.is_some() || wa.error_document.is_some() {
return Err(Error::bad_request( return Err(Error::bad_request(
"Cannot specify indexDocument or errorDocument when disabling website access.", "Cannot specify indexDocument or errorDocument when disabling website access.",
)); ));
} }
state.website_config.update(None.into()); state.website_config.update(None);
} }
} }
@@ -344,38 +322,6 @@ impl RequestHandler for UpdateBucketRequest {
}); });
} }
if let Some(cr) = self.body.cors_rules {
let cors_config = if cr.is_empty() {
None
} else {
let cc = xml::cors::CorsConfiguration {
xmlns: (),
cors_rules: cr,
};
cc.validate()?;
Some(cc.into_garage_cors_config()?)
};
state.cors_config.update(cors_config.into());
}
if let Some(lr) = self.body.lifecycle_rules {
let lifecycle_config = if lr.is_empty() {
None
} else {
let lc = xml::lifecycle::LifecycleConfiguration {
xmlns: (),
lifecycle_rules: lr,
};
Some(
lc.validate_into_garage_lifecycle_config()
.ok_or_bad_request("Invalid lifecycle configuration")?,
)
};
state.lifecycle_config.update(lifecycle_config.into());
}
garage.bucket_table.insert(&bucket).await?; garage.bucket_table.insert(&bucket).await?;
Ok(UpdateBucketResponse( Ok(UpdateBucketResponse(
@@ -611,7 +557,7 @@ impl RequestHandler for AddBucketAliasRequest {
BucketAliasEnum::Global { global_alias } => { BucketAliasEnum::Global { global_alias } => {
helper helper
.set_global_bucket_alias(bucket_id, &global_alias) .set_global_bucket_alias(bucket_id, &global_alias)
.await?; .await?
} }
BucketAliasEnum::Local { BucketAliasEnum::Local {
local_alias, local_alias,
@@ -619,7 +565,7 @@ impl RequestHandler for AddBucketAliasRequest {
} => { } => {
helper helper
.set_local_bucket_alias(bucket_id, &access_key_id, &local_alias) .set_local_bucket_alias(bucket_id, &access_key_id, &local_alias)
.await?; .await?
} }
} }
@@ -645,7 +591,7 @@ impl RequestHandler for RemoveBucketAliasRequest {
BucketAliasEnum::Global { global_alias } => { BucketAliasEnum::Global { global_alias } => {
helper helper
.unset_global_bucket_alias(bucket_id, &global_alias) .unset_global_bucket_alias(bucket_id, &global_alias)
.await?; .await?
} }
BucketAliasEnum::Local { BucketAliasEnum::Local {
local_alias, local_alias,
@@ -653,7 +599,7 @@ impl RequestHandler for RemoveBucketAliasRequest {
} => { } => {
helper helper
.unset_local_bucket_alias(bucket_id, &access_key_id, &local_alias) .unset_local_bucket_alias(bucket_id, &access_key_id, &local_alias)
.await?; .await?
} }
} }
@@ -690,36 +636,45 @@ async fn bucket_info_results(
.map(|x| x.filtered_values(&garage.system.cluster_layout())) .map(|x| x.filtered_values(&garage.system.cluster_layout()))
.unwrap_or_default(); .unwrap_or_default();
let state = bucket.state.as_option().unwrap(); let mut relevant_keys = HashMap::new();
for (k, _) in bucket
let keys1 = state .state
.as_option()
.unwrap()
.authorized_keys .authorized_keys
.items() .items()
.iter() .iter()
.filter(|(_, p)| p.is_any()) {
.map(|(k, _)| k); if let Some(key) = garage
let keys2 = state .key_table
.get(&EmptyKey, k)
.await?
.filter(|k| !k.is_deleted())
{
if !key.state.is_deleted() {
relevant_keys.insert(k.clone(), key);
}
}
}
for ((k, _), _, _) in bucket
.state
.as_option()
.unwrap()
.local_aliases .local_aliases
.items() .items()
.iter() .iter()
.filter(|(_, _, p)| *p) {
.map(|((k, _), _, _)| k); if relevant_keys.contains_key(k) {
let mut relevant_keys = HashMap::new();
for key_id in keys1.chain(keys2) {
if relevant_keys.contains_key(key_id) {
continue; continue;
} }
if let Some(key) = garage.key_table.get(&EmptyKey, key_id).await? { if let Some(key) = garage.key_table.get(&EmptyKey, k).await? {
relevant_keys.insert(key_id.clone(), key); if !key.state.is_deleted() {
} else { relevant_keys.insert(k.clone(), key);
warn!(
"Bucket {:?} references non-existent key {}",
bucket.id, key_id
);
} }
} }
relevant_keys.retain(|_, k| !k.is_deleted()); }
let state = bucket.state.as_option().unwrap();
let quotas = state.quotas.get(); let quotas = state.quotas.get();
let res = GetBucketInfoResponse { let res = GetBucketInfoResponse {
@@ -733,56 +688,38 @@ async fn bucket_info_results(
.filter(|(_, _, a)| *a) .filter(|(_, _, a)| *a)
.map(|(n, _, _)| n.to_string()) .map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
website_access: state.website_config.get().inner().is_some(), website_access: state.website_config.get().is_some(),
website_config: state.website_config.get().inner().cloned().map(|wsc| { website_config: state.website_config.get().clone().map(|wsc| {
GetBucketInfoWebsiteResponse { GetBucketInfoWebsiteResponse {
index_document: wsc.index_document, index_document: wsc.index_document,
error_document: wsc.error_document, error_document: wsc.error_document,
routing_rules: Some(
wsc.routing_rules
.into_iter()
.map(xml::website::RoutingRule::from_garage_routing_rule)
.collect::<Vec<_>>(),
),
} }
}), }),
cors_rules: state.cors_config.get().inner().map(|rules| {
rules
.iter()
.map(xml::cors::CorsRule::from_garage_cors_rule)
.collect::<Vec<_>>()
}),
lifecycle_rules: state.lifecycle_config.get().inner().map(|lc| {
lc.iter()
.map(xml::lifecycle::LifecycleRule::from_garage_lifecycle_rule)
.collect::<Vec<_>>()
}),
keys: relevant_keys keys: relevant_keys
.into_values() .into_values()
.map(|key| { .filter_map(|key| {
let st = key.state.as_option().unwrap(); let p = key.state.as_option().unwrap();
let permissions = st let permissions = p
.authorized_buckets .authorized_buckets
.get(&bucket.id) .get(&bucket.id)
.filter(|p| p.is_any())
.map(|p| ApiBucketKeyPerm { .map(|p| ApiBucketKeyPerm {
read: p.allow_read, read: p.allow_read,
write: p.allow_write, write: p.allow_write,
owner: p.allow_owner, owner: p.allow_owner,
}) })?;
.unwrap_or_default(); Some(GetBucketInfoKey {
let bucket_local_aliases = st access_key_id: key.key_id,
name: p.name.get().to_string(),
permissions,
bucket_local_aliases: p
.local_aliases .local_aliases
.items() .items()
.iter() .iter()
.filter(|(_, _, b)| b.into_inner() == Some(bucket.id)) .filter(|(_, _, b)| *b == Some(bucket.id))
.map(|(n, _, _)| n.to_string()) .map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>(),
GetBucketInfoKey { })
access_key_id: key.key_id,
name: st.name.get().to_string(),
permissions,
bucket_local_aliases,
}
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
objects: *counters.get(OBJECTS).unwrap_or(&0), objects: *counters.get(OBJECTS).unwrap_or(&0),
+17 -94
View File
@@ -8,10 +8,8 @@ use garage_util::data::*;
use garage_rpc::layout; use garage_rpc::layout;
use garage_rpc::layout::PARTITION_BITS; use garage_rpc::layout::PARTITION_BITS;
use garage_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::s3::object_table;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -154,6 +152,7 @@ impl RequestHandler for GetClusterHealthRequest {
impl RequestHandler for GetClusterStatisticsRequest { impl RequestHandler for GetClusterStatisticsRequest {
type Response = GetClusterStatisticsResponse; type Response = GetClusterStatisticsResponse;
// FIXME: return this as a JSON struct instead of text
async fn handle( async fn handle(
self, self,
garage: &Arc<Garage>, garage: &Arc<Garage>,
@@ -161,60 +160,8 @@ impl RequestHandler for GetClusterStatisticsRequest {
) -> Result<GetClusterStatisticsResponse, Error> { ) -> Result<GetClusterStatisticsResponse, Error> {
let mut ret = String::new(); let mut ret = String::new();
// Gather info on number of buckets, objects and object size
let buckets = garage
.bucket_table
.get_range(
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
1_000_000,
EnumerationOrder::Forward,
)
.await?;
let bucket_stats_opt = if buckets.len() < 1000 {
futures::future::try_join_all(
buckets
.iter()
.map(|b| garage.object_counter_table.table.get(&b.id, &EmptyKey)),
)
.await
.ok()
} else {
None
};
let layout = &garage.system.cluster_layout();
let bucket_count = buckets.len() as u64;
let (total_object_count, total_object_bytes);
if let Some(bucket_stats) = bucket_stats_opt {
let bucket_stats = bucket_stats
.into_iter()
.filter_map(|cnt| cnt.map(|x| x.filtered_values(layout)))
.collect::<Vec<_>>();
total_object_count = Some(
bucket_stats
.iter()
.clone()
.map(|cnt| *cnt.get(object_table::OBJECTS).unwrap_or(&0) as u64)
.sum(),
);
total_object_bytes = Some(
bucket_stats
.iter()
.clone()
.map(|cnt| *cnt.get(object_table::BYTES).unwrap_or(&0) as u64)
.sum(),
);
} else {
total_object_count = None;
total_object_bytes = None;
}
// Gather storage node and free space statistics for current nodes // Gather storage node and free space statistics for current nodes
let layout = &garage.system.cluster_layout();
let mut node_partition_count = HashMap::<Uuid, u64>::new(); let mut node_partition_count = HashMap::<Uuid, u64>::new();
if let Ok(current_layout) = layout.current() { if let Ok(current_layout) = layout.current() {
for short_id in current_layout.ring_assignment_data.iter() { for short_id in current_layout.ring_assignment_data.iter() {
@@ -284,57 +231,33 @@ impl RequestHandler for GetClusterStatisticsRequest {
.map(|c| c.0 / *parts) .map(|c| c.0 / *parts)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !meta_part_avail.is_empty() && !data_part_avail.is_empty() {
let metadata_avail: u64 = let meta_avail =
meta_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS); bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
let data_avail: u64 = data_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS); let data_avail =
bytesize::ByteSize(data_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
let metadata_avail_str = bytesize::ByteSize(metadata_avail);
let data_avail_str = bytesize::ByteSize(data_avail);
let incomplete_info = meta_part_avail.len() < node_partition_count.len()
|| data_part_avail.len() < node_partition_count.len();
// Display bucket statistics
let mut bucket_stats = vec![format!("Number of buckets:\t{}", bucket_count)];
if let Some(toc) = total_object_count {
bucket_stats.push(format!("Total number of objects:\t{}", toc));
}
if let Some(tob) = total_object_bytes {
bucket_stats.push(format!(
"Total size of objects:\t{}",
bytesize::ByteSize(tob)
));
}
writeln!(&mut ret, "\n{}", format_table_to_string(bucket_stats)).unwrap();
writeln!( writeln!(
&mut ret, &mut ret,
"Estimated available storage space cluster-wide (might be lower in practice):" "\nEstimated available storage space cluster-wide (might be lower in practice):"
) )
.unwrap(); .unwrap();
if incomplete_info { if meta_part_avail.len() < node_partition_count.len()
|| data_part_avail.len() < node_partition_count.len()
{
ret += &format_table_to_string(vec![ ret += &format_table_to_string(vec![
format!(" data: < {}", data_avail_str), format!(" data: < {}", data_avail),
format!(" metadata: < {}", metadata_avail_str), format!(" metadata: < {}", meta_avail),
]); ]);
writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap(); writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap();
} else { } else {
ret += &format_table_to_string(vec![ ret += &format_table_to_string(vec![
format!(" data: {}", data_avail_str), format!(" data: {}", data_avail),
format!(" metadata: {}", metadata_avail_str), format!(" metadata: {}", meta_avail),
]); ]);
} }
}
Ok(GetClusterStatisticsResponse { Ok(GetClusterStatisticsResponse { freeform: ret })
freeform: ret,
metadata_avail: Some(metadata_avail),
data_avail: Some(data_avail),
incomplete_avail_info: Some(incomplete_info),
bucket_count: Some(bucket_count),
total_object_count,
total_object_bytes,
})
} }
} }
+24 -32
View File
@@ -8,7 +8,6 @@ use garage_util::time::now_msec;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::*; use garage_model::key_table::*;
use garage_model::permission::ExpirationTime;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -41,8 +40,8 @@ impl RequestHandler for ListKeysRequest {
DateTime::from_timestamp_millis(x as i64) DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db") .expect("invalid timestamp stored in db")
}), }),
expiration: p.expiration.get().inner().map(|x| { expiration: p.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x.0 as i64) DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db") .expect("invalid timestamp stored in db")
}), }),
expired: p.is_expired(now), expired: p.is_expired(now),
@@ -77,9 +76,7 @@ impl RequestHandler for GetKeyInfoRequest {
.await? .await?
.into_iter() .into_iter()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if candidates.is_empty() { if candidates.len() != 1 {
return Err(Error::NoSuchAccessKey(search.clone()));
} else if candidates.len() != 1 {
return Err(Error::bad_request(format!( return Err(Error::bad_request(format!(
"{} matching keys", "{} matching keys",
candidates.len() candidates.len()
@@ -188,42 +185,38 @@ async fn key_info_results(
key: Key, key: Key,
show_secret: bool, show_secret: bool,
) -> Result<GetKeyInfoResponse, Error> { ) -> Result<GetKeyInfoResponse, Error> {
let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap(); let key_state = key.state.as_option().unwrap();
let buckets1 = key_state for id in key_state
.authorized_buckets .authorized_buckets
.items() .items()
.iter() .iter()
.filter(|(_, p)| p.is_any()) .map(|(id, _)| id)
.map(|(id, _)| id); .chain(
let buckets2 = key_state key_state
.local_aliases .local_aliases
.items() .items()
.iter() .iter()
.filter_map(|(_, _, v)| v.inner()); .filter_map(|(_, _, v)| v.as_ref()),
) {
let mut relevant_buckets = HashMap::new(); if !relevant_buckets.contains_key(id) {
for bucket_id in buckets1.chain(buckets2) { if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? {
if !relevant_buckets.contains_key(bucket_id) { if b.state.as_option().is_some() {
if let Some(b) = garage.bucket_table.get(&EmptyKey, bucket_id).await? { relevant_buckets.insert(*id, b);
relevant_buckets.insert(*bucket_id, b); }
} else {
warn!(
"Key {} references non-existent bucket {:?}",
key.key_id, bucket_id
);
} }
} }
} }
relevant_buckets.retain(|_, b| !b.is_deleted());
let res = GetKeyInfoResponse { let res = GetKeyInfoResponse {
name: key_state.name.get().clone(), name: key_state.name.get().clone(),
created: key_state.created.map(|x| { created: key_state.created.map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}), }),
expiration: key_state.expiration.get().inner().map(|x| { expiration: key_state.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}), }),
expired: key_state.is_expired(now_msec()), expired: key_state.is_expired(now_msec()),
access_key_id: key.key_id.clone(), access_key_id: key.key_id.clone(),
@@ -237,7 +230,7 @@ async fn key_info_results(
}, },
buckets: relevant_buckets buckets: relevant_buckets
.into_values() .into_values()
.map(|bucket| { .filter_map(|bucket| {
let state = bucket.state.as_option().unwrap(); let state = bucket.state.as_option().unwrap();
let permissions = key_state let permissions = key_state
.authorized_buckets .authorized_buckets
@@ -247,9 +240,8 @@ async fn key_info_results(
read: p.allow_read, read: p.allow_read,
write: p.allow_write, write: p.allow_write,
owner: p.allow_owner, owner: p.allow_owner,
}) })?;
.unwrap_or_default(); Some(KeyInfoBucketResponse {
KeyInfoBucketResponse {
id: hex::encode(bucket.id), id: hex::encode(bucket.id),
global_aliases: state global_aliases: state
.aliases .aliases
@@ -266,7 +258,7 @@ async fn key_info_results(
.map(|((_, n), _, _)| n.to_string()) .map(|((_, n), _, _)| n.to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
permissions, permissions,
} })
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}; };
@@ -289,10 +281,10 @@ fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(),
if let Some(expiration) = updates.expiration { if let Some(expiration) = updates.expiration {
key_state key_state
.expiration .expiration
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into()); .update(Some(expiration.timestamp_millis() as u64));
} }
if updates.never_expires { if updates.never_expires {
key_state.expiration.update(None.into()); key_state.expiration.update(None);
} }
if let Some(allow) = updates.allow { if let Some(allow) = updates.allow {
if allow.create_bucket { if allow.create_bucket {
+6 -12
View File
@@ -53,7 +53,7 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp
.roles .roles
.items() .items()
.iter() .iter()
.filter(|(k, _, v)| current.roles.get(k).and_then(|vv| vv.0.as_ref()) != v.0.as_ref()) .filter(|(k, _, v)| current.roles.get(k) != Some(v))
.map(|(k, _, v)| match &v.0 { .map(|(k, _, v)| match &v.0 {
None => NodeRoleChange { None => NodeRoleChange {
id: hex::encode(k), id: hex::encode(k),
@@ -255,14 +255,10 @@ impl RequestHandler for PreviewClusterLayoutChangesRequest {
Ok(PreviewClusterLayoutChangesResponse::Error { error }) Ok(PreviewClusterLayoutChangesResponse::Error { error })
} }
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
Ok((new_layout, stat)) => { Ok((new_layout, msg)) => Ok(PreviewClusterLayoutChangesResponse::Success {
let message = stat.to_message(); message: msg,
Ok(PreviewClusterLayoutChangesResponse::Success {
message,
statistics: Some(Box::new(stat)),
new_layout: format_cluster_layout(&new_layout), new_layout: format_cluster_layout(&new_layout),
}) }),
}
} }
} }
} }
@@ -276,8 +272,7 @@ impl RequestHandler for ApplyClusterLayoutRequest {
_admin: &Admin, _admin: &Admin,
) -> Result<ApplyClusterLayoutResponse, Error> { ) -> Result<ApplyClusterLayoutResponse, Error> {
let layout = garage.system.cluster_layout().inner().clone(); let layout = garage.system.cluster_layout().inner().clone();
let (layout, stat) = layout.apply_staged_changes(self.version)?; let (layout, msg) = layout.apply_staged_changes(self.version)?;
let message = stat.to_message();
garage garage
.system .system
@@ -286,8 +281,7 @@ impl RequestHandler for ApplyClusterLayoutRequest {
.await?; .await?;
Ok(ApplyClusterLayoutResponse { Ok(ApplyClusterLayoutResponse {
message, message: msg,
statistics: Some(stat),
layout: format_cluster_layout(&layout), layout: format_cluster_layout(&layout),
}) })
} }
+55 -115
View File
@@ -22,55 +22,13 @@ impl RequestHandler for LocalGetNodeInfoRequest {
garage: &Arc<Garage>, garage: &Arc<Garage>,
_admin: &Admin, _admin: &Admin,
) -> Result<LocalGetNodeInfoResponse, Error> { ) -> Result<LocalGetNodeInfoResponse, Error> {
let sys_status = garage.system.local_status();
let hostname = sys_status.hostname.unwrap_or_default().to_string();
let layout = garage.system.cluster_layout();
let current_layout = layout.inner().current();
Ok(LocalGetNodeInfoResponse { Ok(LocalGetNodeInfoResponse {
node_id: hex::encode(garage.system.id), node_id: hex::encode(garage.system.id),
hostname: Some(hostname),
garage_version: garage_util::version::garage_version().to_string(), garage_version: garage_util::version::garage_version().to_string(),
garage_features: garage_util::version::garage_features() garage_features: garage_util::version::garage_features()
.map(|features| features.iter().map(ToString::to_string).collect()), .map(|features| features.iter().map(ToString::to_string).collect()),
rust_version: garage_util::version::rust_version().to_string(), rust_version: garage_util::version::rust_version().to_string(),
db_engine: garage.db.engine(), db_engine: garage.db.engine(),
is_up: Some(true),
addr: garage
.system
.get_known_nodes()
.iter()
.find(|x| x.id == garage.system.id)
.and_then(|x| x.addr),
draining: Some(
current_layout.node_role(&garage.system.id).is_none()
&& layout
.inner()
.versions
.iter()
.filter(|x| x.version != current_layout.version)
.any(|x| x.node_role(&garage.system.id).is_some()),
),
role: current_layout
.node_role(&garage.system.id)
.map(|v| NodeAssignedRole {
zone: v.zone.clone(),
capacity: v.capacity,
tags: v.tags.clone(),
}),
data_partition: sys_status
.data_disk_avail
.map(|(avail, total)| FreeSpaceResp {
available: avail,
total,
}),
metadata_partition: sys_status
.meta_disk_avail
.map(|(avail, total)| FreeSpaceResp {
available: avail,
total,
}),
}) })
} }
} }
@@ -99,57 +57,45 @@ impl RequestHandler for LocalGetNodeStatisticsRequest {
) -> Result<LocalGetNodeStatisticsResponse, Error> { ) -> Result<LocalGetNodeStatisticsResponse, Error> {
let sys_status = garage.system.local_status(); let sys_status = garage.system.local_status();
let hostname = sys_status.hostname.unwrap_or_default().to_string();
let garage_version = garage_util::version::garage_version().to_string();
let garage_features = garage_util::version::garage_features()
.unwrap()
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>();
let rustc_version = garage_util::version::rust_version().to_string();
let db_engine_descr = garage.db.engine();
let mut ret = format_table_to_string(vec![ let mut ret = format_table_to_string(vec![
format!("Node ID:\t{:?}", garage.system.id), format!("Node ID:\t{:?}", garage.system.id),
format!("Hostname:\t{}", hostname), format!("Hostname:\t{}", sys_status.hostname.unwrap_or_default(),),
format!("Garage version:\t{}", garage_version), format!(
format!("Garage features:\t{}", garage_features.join(", ")), "Garage version:\t{}",
format!("Rust compiler version:\t{}", rustc_version), garage_util::version::garage_version(),
format!("Database engine:\t{}", db_engine_descr), ),
format!(
"Garage features:\t{}",
garage_util::version::garage_features()
.map(|list| list.join(", "))
.unwrap_or_else(|| "(unknown)".into()),
),
format!(
"Rust compiler version:\t{}",
garage_util::version::rust_version(),
),
format!("Database engine:\t{}", garage.db.engine()),
]); ]);
let mut table_stats = vec![
gather_table_stats(&garage.admin_token_table)?,
gather_table_stats(&garage.bucket_table)?,
gather_table_stats(&garage.bucket_alias_table)?,
gather_table_stats(&garage.key_table)?,
gather_table_stats(&garage.object_table)?,
gather_table_stats(&garage.object_counter_table.table)?,
gather_table_stats(&garage.mpu_table)?,
gather_table_stats(&garage.mpu_counter_table.table)?,
gather_table_stats(&garage.version_table)?,
gather_table_stats(&garage.block_ref_table)?,
];
#[cfg(feature = "k2v")]
{
table_stats.push(gather_table_stats(&garage.k2v.item_table)?);
table_stats.push(gather_table_stats(&garage.k2v.counter_table.table)?);
}
// Gather table statistics // Gather table statistics
let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()]; let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()];
table.extend(table_stats.iter().map(|ts| { table.push(gather_table_stats(&garage.admin_token_table)?);
format!( table.push(gather_table_stats(&garage.bucket_table)?);
" {}\t{}\t{}\t{}\t{}\t{}", table.push(gather_table_stats(&garage.bucket_alias_table)?);
ts.table_name, table.push(gather_table_stats(&garage.key_table)?);
ts.items,
ts.merkle_items, table.push(gather_table_stats(&garage.object_table)?);
ts.merkle_queue_len, table.push(gather_table_stats(&garage.object_counter_table.table)?);
ts.insert_queue_len, table.push(gather_table_stats(&garage.mpu_table)?);
ts.gc_queue_len, table.push(gather_table_stats(&garage.mpu_counter_table.table)?);
) table.push(gather_table_stats(&garage.version_table)?);
})); table.push(gather_table_stats(&garage.block_ref_table)?);
#[cfg(feature = "k2v")]
{
table.push(gather_table_stats(&garage.k2v.item_table)?);
table.push(gather_table_stats(&garage.k2v.counter_table.table)?);
}
write!( write!(
&mut ret, &mut ret,
@@ -158,52 +104,46 @@ impl RequestHandler for LocalGetNodeStatisticsRequest {
) )
.unwrap(); .unwrap();
let block_manager_stats = NodeBlockManagerStats {
rc_entries: garage.block_manager.rc_approximate_len()? as u64,
resync_queue_len: garage.block_manager.resync.queue_approximate_len()? as u64,
resync_errors: garage.block_manager.resync.errors_approximate_len()? as u64,
};
// Gather block manager statistics // Gather block manager statistics
writeln!(&mut ret, "\nBlock manager stats:").unwrap(); writeln!(&mut ret, "\nBlock manager stats:").unwrap();
let rc_len = garage.block_manager.rc_approximate_len()?.to_string();
ret += &format_table_to_string(vec![ ret += &format_table_to_string(vec![
format!( format!(" number of RC entries:\t{} (~= number of blocks)", rc_len),
" number of RC entries:\t{} (~= number of blocks)",
block_manager_stats.rc_entries
),
format!( format!(
" resync queue length:\t{}", " resync queue length:\t{}",
block_manager_stats.resync_queue_len, garage.block_manager.resync.queue_approximate_len()?
), ),
format!( format!(
" blocks with resync errors:\t{}", " blocks with resync errors:\t{}",
block_manager_stats.resync_errors garage.block_manager.resync.errors_approximate_len()?
), ),
]); ]);
Ok(LocalGetNodeStatisticsResponse { Ok(LocalGetNodeStatisticsResponse { freeform: ret })
freeform: ret,
table_stats: Some(table_stats),
block_manager_stats: Some(block_manager_stats),
})
} }
} }
fn gather_table_stats<F, R>(t: &Arc<Table<F, R>>) -> Result<NodeTableStats, Error> fn gather_table_stats<F, R>(t: &Arc<Table<F, R>>) -> Result<String, Error>
where where
F: TableSchema + 'static, F: TableSchema + 'static,
R: TableReplication + 'static, R: TableReplication + 'static,
{ {
let data_len = t.data.store.approximate_len().map_err(GarageError::from)?; let data_len = t
let mkl_len = t.merkle_updater.merkle_tree_approximate_len()?; .data
.store
.approximate_len()
.map_err(GarageError::from)?
.to_string();
let mkl_len = t.merkle_updater.merkle_tree_approximate_len()?.to_string();
Ok(NodeTableStats { Ok(format!(
table_name: F::TABLE_NAME.to_string(), " {}\t{}\t{}\t{}\t{}\t{}",
items: data_len as u64, F::TABLE_NAME,
merkle_items: mkl_len as u64, data_len,
merkle_queue_len: t.merkle_updater.todo_approximate_len()? as u64, mkl_len,
insert_queue_len: t.data.insert_queue_approximate_len()? as u64, t.merkle_updater.todo_approximate_len()?,
gc_queue_len: t.data.gc_todo_approximate_len()? as u64, t.data.insert_queue_approximate_len()?,
}) t.data.gc_todo_approximate_len()?
))
} }
+2 -2
View File
@@ -869,14 +869,14 @@ impl Modify for SecurityAddon {
components.add_security_scheme( components.add_security_scheme(
"bearerAuth", "bearerAuth",
SecurityScheme::Http(Http::builder().scheme(HttpAuthScheme::Bearer).build()), SecurityScheme::Http(Http::builder().scheme(HttpAuthScheme::Bearer).build()),
); )
} }
} }
#[derive(OpenApi)] #[derive(OpenApi)]
#[openapi( #[openapi(
info( info(
version = "v2.3.0", version = "v2.2.0",
title = "Garage administration API", title = "Garage administration API",
description = "Administrate your Garage cluster programmatically, including status, layout, keys, buckets, and maintenance tasks. description = "Administrate your Garage cluster programmatically, including status, layout, keys, buckets, and maintenance tasks.
+8 -1
View File
@@ -360,7 +360,14 @@ impl Worker for BlockRcRepair {
_must_exit: &mut watch::Receiver<bool>, _must_exit: &mut watch::Receiver<bool>,
) -> Result<WorkerState, GarageError> { ) -> Result<WorkerState, GarageError> {
for _i in 0..RC_REPAIR_ITER_COUNT { for _i in 0..RC_REPAIR_ITER_COUNT {
let next1 = self.block_manager.rc.get_first_hash_from(self.cursor)?; let next1 = self
.block_manager
.rc
.rc_table
.range(self.cursor.as_slice()..)?
.next()
.transpose()?
.map(|(k, _)| Hash::try_from(k.as_slice()).unwrap());
let next2 = self let next2 = self
.block_ref_table .block_ref_table
.data .data
+2 -2
View File
@@ -77,7 +77,7 @@ pub enum Endpoint {
impl Endpoint { impl Endpoint {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was /// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header. /// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not `Endpoint::ListBuckets` /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> { pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> {
let uri = req.uri(); let uri = req.uri();
let path = uri.path(); let path = uri.path();
@@ -124,7 +124,7 @@ impl Endpoint {
]); ]);
if let Some(message) = query.nonempty_message() { if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message); debug!("Unused query parameter: {}", message)
} }
Ok(res) Ok(res)
+2 -2
View File
@@ -79,7 +79,7 @@ pub enum Endpoint {
impl Endpoint { impl Endpoint {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was /// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header. /// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not `Endpoint::ListBuckets` /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> { pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> {
let uri = req.uri(); let uri = req.uri();
let path = uri.path(); let path = uri.path();
@@ -126,7 +126,7 @@ impl Endpoint {
]); ]);
if let Some(message) = query.nonempty_message() { if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message); debug!("Unused query parameter: {}", message)
} }
Ok(res) Ok(res)
+2 -2
View File
@@ -15,7 +15,7 @@ use crate::Authorization;
impl AdminApiRequest { impl AdminApiRequest {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was /// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header. /// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not `Endpoint::ListBuckets` /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub async fn from_request(req: Request<IncomingBody>) -> Result<Self, Error> { pub async fn from_request(req: Request<IncomingBody>) -> Result<Self, Error> {
let uri = req.uri().clone(); let uri = req.uri().clone();
let path = uri.path(); let path = uri.path();
@@ -89,7 +89,7 @@ impl AdminApiRequest {
]); ]);
if let Some(message) = query.nonempty_message() { if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message); debug!("Unused query parameter: {}", message)
} }
Ok(res) Ok(res)
+1 -1
View File
@@ -164,7 +164,7 @@ async fn check_domain(garage: &Arc<Garage>, domain: &str) -> Result<bool, Error>
} }
let bucket_state = bucket.state.as_option().unwrap(); let bucket_state = bucket.state.as_option().unwrap();
let bucket_website_config = bucket_state.website_config.get().inner(); let bucket_website_config = bucket_state.website_config.get();
match bucket_website_config { match bucket_website_config {
Some(_v) => Ok(true), Some(_v) => Ok(true),
+1 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_common" name = "garage_api_common"
version = "2.3.0" version = "2.2.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -27,7 +27,6 @@ thiserror.workspace = true
hex.workspace = true hex.workspace = true
hmac.workspace = true hmac.workspace = true
md-5.workspace = true md-5.workspace = true
percent-encoding.workspace = true
tracing.workspace = true tracing.workspace = true
nom.workspace = true nom.workspace = true
pin-project.workspace = true pin-project.workspace = true
@@ -42,12 +41,7 @@ hyper = { workspace = true, default-features = false, features = ["server", "htt
hyper-util.workspace = true hyper-util.workspace = true
url.workspace = true url.workspace = true
quick-xml.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
utoipa.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
[lints]
workspace = true
+5 -18
View File
@@ -36,10 +36,6 @@ pub enum CommonError {
#[error("Invalid header value: {0}")] #[error("Invalid header value: {0}")]
InvalidHeader(#[from] hyper::header::ToStrError), InvalidHeader(#[from] hyper::header::ToStrError),
/// The client sent a request for an action not supported by garage
#[error("Unimplemented action: {0}")]
NotImplemented(String),
// ---- SPECIFIC ERROR CONDITIONS ---- // ---- SPECIFIC ERROR CONDITIONS ----
// These have to be error codes referenced in the S3 spec here: // These have to be error codes referenced in the S3 spec here:
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
@@ -59,10 +55,6 @@ pub enum CommonError {
/// Bucket name is not valid according to AWS S3 specs /// Bucket name is not valid according to AWS S3 specs
#[error("Invalid bucket name: {0}")] #[error("Invalid bucket name: {0}")]
InvalidBucketName(String), InvalidBucketName(String),
/// Tried to create bucket that is already owned by you
#[error("Bucket already owned by you")]
BucketAlreadyOwnedByYou,
} }
#[macro_export] #[macro_export]
@@ -105,11 +97,8 @@ impl CommonError {
} }
CommonError::BadRequest(_) => StatusCode::BAD_REQUEST, CommonError::BadRequest(_) => StatusCode::BAD_REQUEST,
CommonError::Forbidden(_) => StatusCode::FORBIDDEN, CommonError::Forbidden(_) => StatusCode::FORBIDDEN,
CommonError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND, CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND,
CommonError::BucketNotEmpty CommonError::BucketNotEmpty | CommonError::BucketAlreadyExists => StatusCode::CONFLICT,
| CommonError::BucketAlreadyExists
| CommonError::BucketAlreadyOwnedByYou => StatusCode::CONFLICT,
CommonError::InvalidBucketName(_) | CommonError::InvalidHeader(_) => { CommonError::InvalidBucketName(_) | CommonError::InvalidHeader(_) => {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }
@@ -131,8 +120,6 @@ impl CommonError {
CommonError::BucketNotEmpty => "BucketNotEmpty", CommonError::BucketNotEmpty => "BucketNotEmpty",
CommonError::InvalidBucketName(_) => "InvalidBucketName", CommonError::InvalidBucketName(_) => "InvalidBucketName",
CommonError::InvalidHeader(_) => "InvalidHeaderValue", CommonError::InvalidHeader(_) => "InvalidHeaderValue",
CommonError::BucketAlreadyOwnedByYou => "BucketAlreadyOwnedByYou",
CommonError::NotImplemented(_) => "NotImplemented",
} }
} }
@@ -155,10 +142,10 @@ impl TryFrom<HelperError> for CommonError {
} }
} }
/// This function converts `HelperErrors` into `CommonErrors`, /// This function converts HelperErrors into CommonErrors,
/// for variants that exist in `CommonError`. /// for variants that exist in CommonError.
/// This is used for helper functions that might return `InvalidBucketName` /// This is used for helper functions that might return InvalidBucketName
/// or `NoSuchBucket` for instance, and we want to pass that error /// or NoSuchBucket for instance, and we want to pass that error
/// up to our caller. /// up to our caller.
pub fn pass_helper_error(err: HelperError) -> CommonError { pub fn pass_helper_error(err: HelperError) -> CommonError {
match CommonError::try_from(err) { match CommonError::try_from(err) {

Some files were not shown because too many files have changed in this diff Show More