Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19a29cce6d | |||
| d65be95051 | |||
| ae2404027a | |||
| 13d313134d | |||
| 05e294307e | |||
| 109fbd49b1 | |||
| d0176f8e30 | |||
| d9b1dba137 | |||
| c3c8af9a66 | |||
| ad78b9ee5c | |||
| b8e844612d | |||
| aa4e4656b1 | |||
| 5549077d6f | |||
| 663fc5ae48 | |||
| 0f89923d2d | |||
| a1f9c52563 | |||
| ceade99cd7 | |||
| d1333c1729 | |||
| 7e246b20e9 | |||
| 936e53fe2f | |||
| bbc1dc3d6f | |||
| 7cdcaa8ebe | |||
| 4133476ec9 | |||
| b277d49ad6 | |||
| 5a4da29f92 | |||
| eb91f463f5 | |||
| 22e003f0d0 | |||
| 0ebf60f709 | |||
| d588a96125 | |||
| e665a3432c | |||
| 5884b4e9c7 | |||
| a32621c8fd | |||
| 8eb7628bf4 | |||
| cd26594673 | |||
| 1b28094f20 | |||
| 4f9faeb282 | |||
| 3c7990027a | |||
| a159c1c483 | |||
| 555e0826a2 | |||
| d0f89068c6 | |||
| 3e25914210 | |||
| 5500f1c412 | |||
| 2d7c0a6087 | |||
| 76aae3cb96 | |||
| 1d1456f1d6 | |||
| a379406522 | |||
| da568cf56f | |||
| 2bde733e09 | |||
| 91573eb028 | |||
| a646180d7e | |||
| bacc6c98b2 | |||
| bf0a24ea69 | |||
| eb37a3e11a | |||
| 54c63387cb | |||
| 84bdc9f50f | |||
| 3a5f060693 | |||
| 21d29a4cf6 | |||
| a0887afc4f | |||
| f757991635 | |||
| 0da317e3d5 | |||
| 57ceed38f3 | |||
| 0eb7d61829 | |||
| 382981642d | |||
| 28a75d7234 | |||
| e996f34887 | |||
| defaac1b4f | |||
| 9f157677c2 | |||
| ddc42c89fb | |||
| a25ad494cc | |||
| 7d97b2b96e | |||
| a5650ea303 | |||
| 322da7242b | |||
| 6a097e7de3 | |||
| 6ddae5397c | |||
| 9a18259419 | |||
| ade4d07bb5 | |||
| 0a5282d918 | |||
| 12012916b7 | |||
| 9fa4e03748 | |||
| f7be222471 | |||
| 5e9380820e | |||
| 62349a6559 | |||
| ada0c8ab70 | |||
| 7bc7f33f43 | |||
| be203494c5 | |||
| 3c983ac5e0 | |||
| a2c797000f | |||
| 80f9335950 | |||
| d217a3f15d | |||
| 063bf8258b | |||
| 1d66240495 | |||
| d977ca4a24 | |||
| 1cdaccbc3d | |||
| 8e38680ef5 | |||
| 393c4bb2f6 | |||
| 74ad3bf887 | |||
| 7c18abb664 | |||
| 1dffcca430 | |||
| 5a8ee9f640 | |||
| 7b119c0b4f | |||
| 02d5e67698 | |||
| 854280e957 | |||
| 9ea2b1d628 | |||
| 7b7548a4f7 | |||
| a2e410f8b6 | |||
| 690729ccdb | |||
| ff743453b6 | |||
| f34a7db48a | |||
| 3a355b1617 | |||
| 0b5e82a18b | |||
| 2798667345 | |||
| b1660f0cba | |||
| dfb20ba87f | |||
| 7279cb9113 | |||
| 56cb89d153 | |||
| 6fd9bba0cb | |||
| f9605fae78 | |||
| 9969c3e599 | |||
| a69a8d3b21 | |||
| 3a97b13e2f | |||
| 4efaea60bb | |||
| 06e9756729 | |||
| 8341b7f914 | |||
| 96b986a0a0 | |||
| 60244b60dd | |||
| 9848ec7f4e | |||
| b81eae3f65 | |||
| 6131318c80 | |||
| 4566020360 | |||
| de10dc43d5 | |||
| 8abd0fee86 | |||
| af5f68a34d | |||
| 19e5f83164 | |||
| 64087172ff | |||
| 6c0bb1c9b6 | |||
| 124a9eb521 | |||
| 03e6020c6b | |||
| 836657565e | |||
| 76592723de | |||
| d2f033641e | |||
| 2cfd92e0c3 | |||
| f796df8c34 | |||
| 668dfea4e2 | |||
| 7f61bbbebb | |||
| 8105ca888d | |||
| d0166fe938 | |||
| 290a7f5ab6 | |||
| 2576626240 | |||
| 6591044c2e | |||
| 1ae4e5d438 | |||
| d80096de92 | |||
| 674c2c1cb1 | |||
| 3c5018bd6b | |||
| 93cd71eb72 | |||
| 507be60890 | |||
| 780f389973 | |||
| 69cd230568 | |||
| 55370d9b4d | |||
| ce1ea79bf1 | |||
| 2803c73045 | |||
| 9b3e4716bf | |||
| 83f8bdbacd | |||
| b060a7e0f1 | |||
| f6414210fa | |||
| bbb62dfa85 | |||
| f59a8b7f62 | |||
| 08fd6e659f | |||
| 6cde00073f | |||
| 0043ad08fa | |||
| 0c70c87391 | |||
| b0840ab256 | |||
| 7ad0f96222 | |||
| c373222e3a | |||
| c73268fd77 | |||
| c82015f6cf | |||
| 3af9e8d3d2 | |||
| 1d588282bf | |||
| f7ec4b1338 | |||
| 00cbc5c31d | |||
| 95aa3bc795 | |||
| 53ace58e44 | |||
| c22c4ff2e5 | |||
| 6f511fc149 | |||
| 70b8ebc8b6 | |||
| 02db2d5f9d | |||
| cc749a6290 | |||
| 9f969ef43e | |||
| 4989be7853 | |||
| 8ce4415edb | |||
| 75c28faa5b | |||
| 8644511ebc | |||
| e5627bbd6b | |||
| 3aeb6d88af | |||
| 6f44631973 | |||
| 566a0b44c0 | |||
| 934c4c31f1 | |||
| be8e4c9dcf | |||
| 1b20713421 | |||
| 473b66ca5b | |||
| 5c8a31708e | |||
| ca1211d927 | |||
| b012431df0 | |||
| 6e98f5e74e |
@@ -2,13 +2,14 @@ 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
|
||||||
@@ -52,3 +53,15 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# 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.
|
||||||
@@ -16,6 +16,7 @@ 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"]
|
||||||
@@ -24,108 +25,131 @@ 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.2.0", path = "src/api/common" }
|
garage_api_common = { version = "2.3.0", path = "src/api/common" }
|
||||||
garage_api_admin = { version = "2.2.0", path = "src/api/admin" }
|
garage_api_admin = { version = "2.3.0", path = "src/api/admin" }
|
||||||
garage_api_s3 = { version = "2.2.0", path = "src/api/s3" }
|
garage_api_s3 = { version = "2.3.0", path = "src/api/s3" }
|
||||||
garage_api_k2v = { version = "2.2.0", path = "src/api/k2v" }
|
garage_api_k2v = { version = "2.3.0", path = "src/api/k2v" }
|
||||||
garage_block = { version = "2.2.0", path = "src/block" }
|
garage_block = { version = "2.3.0", path = "src/block" }
|
||||||
garage_db = { version = "2.2.0", path = "src/db", default-features = false }
|
garage_db = { version = "2.3.0", path = "src/db", default-features = false }
|
||||||
garage_model = { version = "2.2.0", path = "src/model", default-features = false }
|
garage_model = { version = "2.3.0", path = "src/model", default-features = false }
|
||||||
garage_net = { version = "2.2.0", path = "src/net" }
|
garage_net = { version = "2.3.0", path = "src/net" }
|
||||||
garage_rpc = { version = "2.2.0", path = "src/rpc" }
|
garage_rpc = { version = "2.3.0", path = "src/rpc" }
|
||||||
garage_table = { version = "2.2.0", path = "src/table" }
|
garage_table = { version = "2.3.0", path = "src/table" }
|
||||||
garage_util = { version = "2.2.0", path = "src/util" }
|
garage_util = { version = "2.3.0", path = "src/util" }
|
||||||
garage_web = { version = "2.2.0", path = "src/web" }
|
garage_web = { version = "2.3.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.1"
|
arc-swap = "1.8"
|
||||||
|
arbitrary = { version = "1.4.2"}
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
async-trait = "0.1.7"
|
async-trait = "0.1"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
blake2 = "0.10"
|
blake2 = "0.10"
|
||||||
bytes = "1.0"
|
bytes = "1.11"
|
||||||
bytesize = "1.1"
|
bytesize = "2.3"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
crc-fast = "1.6"
|
crc-fast = "1.9"
|
||||||
crypto-common = "0.1"
|
crypto-common = "0.1"
|
||||||
gethostname = "0.4"
|
fundu = "2.0"
|
||||||
git-version = "0.3.4"
|
fundu-systemd = "0.3"
|
||||||
|
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.12"
|
itertools = "0.14"
|
||||||
ipnet = "2.9.0"
|
ipnet = "2.11"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.5"
|
||||||
|
libfuzzer-sys = "0.4"
|
||||||
md-5 = "0.10"
|
md-5 = "0.10"
|
||||||
mktemp = "0.5"
|
mktemp = "0.5"
|
||||||
nix = { version = "0.29", default-features = false, features = ["fs"] }
|
nix = { version = "0.31", default-features = false, features = ["fs"] }
|
||||||
nom = "7.1"
|
nom = "8.0"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
parse_duration = "2.1"
|
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
pin-project = "1.0.12"
|
pin-project = "1.1"
|
||||||
pnet_datalink = "0.34"
|
pnet_datalink = "0.35"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
timeago = { version = "0.4", default-features = false }
|
timeago = { version = "0.5", 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.1", features = ["derive", "env"] }
|
clap = { version = "4.5", 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.1"
|
tracing-journald = "0.3"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
heed = { version = "0.22", default-features = false, features = [] }
|
||||||
rusqlite = "0.37"
|
rusqlite = { version = "0.38", features = ["fallible_uint"] }
|
||||||
r2d2 = "0.8"
|
r2d2 = "0.8"
|
||||||
r2d2_sqlite = "0.31"
|
r2d2_sqlite = "0.32"
|
||||||
fjall = "2.4"
|
fjall = "2.11"
|
||||||
|
|
||||||
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.26", features = ["serialize"] }
|
quick-xml = { version = "0.39", features = ["serialize"] }
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.3"
|
||||||
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.8", default-features = false, features = ["parse"] }
|
toml = { version = "0.9", default-features = false, features = ["parse", "serde"] }
|
||||||
utoipa = { version = "5.3.1", features = ["chrono"] }
|
utoipa = { version = "5.4", features = ["chrono"] }
|
||||||
|
|
||||||
# newer version requires rust edition 2021
|
# newer version requires rust edition 2021
|
||||||
k8s-openapi = { version = "0.21", features = ["v1_24"] }
|
k8s-openapi = { version = "0.27", features = ["v1_35"] }
|
||||||
kube = { version = "0.88", default-features = false, features = ["runtime", "derive", "client", "rustls-tls"] }
|
kube = { version = "3.0", default-features = false, features = [
|
||||||
schemars = "0.8"
|
"runtime",
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-manual-roots", "json"] }
|
"derive",
|
||||||
|
"client",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
|
schemars = "1.2"
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = [
|
||||||
|
"rustls-no-provider",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
|
|
||||||
form_urlencoded = "1.0.0"
|
form_urlencoded = "1.2"
|
||||||
http = "1.0"
|
http = "1.4"
|
||||||
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.0", default-features = false }
|
hyper = { version = "1.8", default-features = false }
|
||||||
hyper-util = { version = "0.1", features = ["full"] }
|
hyper-util = { version = "0.1", features = ["full"] }
|
||||||
multer = "3.0"
|
multer = "3.1"
|
||||||
percent-encoding = "2.2"
|
percent-encoding = "2.3"
|
||||||
roxmltree = "0.19"
|
roxmltree = "0.21"
|
||||||
url = "2.3"
|
url = "2.5"
|
||||||
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio = { version = "1.49", default-features = false, features = [
|
||||||
|
"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"
|
||||||
@@ -134,25 +158,42 @@ 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.1", default-features = false }
|
aws-sigv4 = { version = "1.3", default-features = false }
|
||||||
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = [
|
||||||
|
"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.0"
|
rustc_version = "0.4"
|
||||||
static_init = "1.0"
|
static_init = "1.0"
|
||||||
aws-smithy-runtime = { version = "1.8", default-features = false, features = ["tls-rustls"] }
|
aws-smithy-runtime = { version = "1.9", default-features = false, features = [
|
||||||
aws-sdk-config = { version = "1.62", default-features = false }
|
"tls-rustls",
|
||||||
aws-sdk-s3 = { version = "1.79", default-features = false, features = ["rt-tokio"] }
|
] }
|
||||||
|
aws-sdk-config = { version = "1.99", default-features = false }
|
||||||
[profile.dev]
|
aws-sdk-s3 = { version = "1.121", default-features = false, features = [
|
||||||
#lto = "thin" # disabled for now, adds 2-4 min to each CI build
|
"rt-tokio",
|
||||||
lto = "off"
|
] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 16
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
strip = true
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[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
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 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.
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"name": "AGPL-3.0",
|
"name": "AGPL-3.0",
|
||||||
"identifier": "AGPL-3.0"
|
"identifier": "AGPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "v2.2.0"
|
"version": "v2.3.0"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@@ -1797,6 +1797,17 @@
|
|||||||
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2119,6 +2130,180 @@
|
|||||||
"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": {
|
||||||
@@ -2340,6 +2525,16 @@
|
|||||||
"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",
|
||||||
@@ -2363,6 +2558,16 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -2423,6 +2628,15 @@
|
|||||||
},
|
},
|
||||||
"indexDocument": {
|
"indexDocument": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"routingRules": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/website.RoutingRule"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2581,8 +2795,61 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3055,7 +3322,8 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"dbEngine": {
|
"dbEngine": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "database engine used for metadata"
|
||||||
},
|
},
|
||||||
"garageFeatures": {
|
"garageFeatures": {
|
||||||
"type": [
|
"type": [
|
||||||
@@ -3064,16 +3332,26 @@
|
|||||||
],
|
],
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3083,8 +3361,30 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3385,7 +3685,8 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"dbEngine": {
|
"dbEngine": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "database engine used for metadata"
|
||||||
},
|
},
|
||||||
"garageFeatures": {
|
"garageFeatures": {
|
||||||
"type": [
|
"type": [
|
||||||
@@ -3394,16 +3695,26 @@
|
|||||||
],
|
],
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3439,8 +3750,30 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3779,6 +4112,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": [
|
||||||
@@ -3942,6 +4303,53 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": [
|
||||||
@@ -3998,6 +4406,17 @@
|
|||||||
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4117,7 +4536,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\nfunctionnally 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\nfunctionally equivalent to granting a scope of `*`."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4127,6 +4546,24 @@
|
|||||||
"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": [
|
||||||
{
|
{
|
||||||
@@ -4172,6 +4609,15 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"routingRules": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/website.RoutingRule"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4413,6 +4859,205 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
|||||||
@@ -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 new --name ejabberd
|
garage key create 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 new --name pleroma-key
|
garage key create 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 new --name pictrs-key
|
garage key create 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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 new --name my-key
|
garage key create 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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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/prise_en_main/winscp/).*
|
*You can find instructions on how to use the GUI in french [in our wiki](https://guide.deuxfleurs.fr/services/winscp/).*
|
||||||
|
|
||||||
How to use `winscp.com`, the CLI interface of WinSCP:
|
How to use `winscp.com`, the CLI interface of WinSCP:
|
||||||
|
|
||||||
|
|||||||
@@ -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 new --name vector-system-logs
|
garage key create 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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ 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:
|
||||||
|
|||||||
@@ -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.2.0`) and not the `latest` tag.
|
We encourage you to use a fixed tag (eg. `v2.3.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.2.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.3.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:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo docker pull dxflrs/garage:v2.2.0
|
docker pull dxflrs/garage:v2.3.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.2.0
|
dxflrs/garage:v2.3.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.2.0
|
image: dxflrs/garage:v2.3.0
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -142,7 +142,74 @@ server {
|
|||||||
|
|
||||||
## Apache httpd
|
## Apache httpd
|
||||||
|
|
||||||
@TODO
|
The [Apache HTTP Server](https://httpd.apache.org/)
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -82,12 +82,6 @@ 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.**
|
||||||
@@ -124,23 +118,6 @@ 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,15 +3,6 @@ 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.
|
||||||
|
|||||||
@@ -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
|
## Rebalancing data directories {#rebalance}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ 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 repair
|
- Active rebalancing: an operator of a Garage node can [explicitly launch a
|
||||||
procedure that rebalances the data directories, moving all blocks to their
|
repair procedure](@/documentation/operations/durability-repairs.md#rebalance)
|
||||||
primary location. Once done, all secondary locations for all hash slices are
|
that rebalances the data directories, moving all blocks to their primary
|
||||||
removed so that they won't be checked anymore when looking for a data block.
|
location. Once done, all secondary locations for all hash slices are removed
|
||||||
|
so that they won't be checked anymore when looking for a data block.
|
||||||
|
|
||||||
## Read-only storage locations
|
## Read-only storage locations
|
||||||
|
|
||||||
|
|||||||
@@ -43,12 +43,10 @@ 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. When using Docker, the commands used in this guide will not work
|
container. For simplicity, a minimal command to launch Garage using Docker is
|
||||||
anymore. We recommend reading the tutorial on [configuring a
|
provided in this quick start guide. We recommend reading the tutorial on
|
||||||
multi-node cluster](@/documentation/cookbook/real-world.md) to learn about
|
[configuring a multi-node cluster](@/documentation/cookbook/real-world.md) to
|
||||||
using Garage as a Docker container. For simplicity, a minimal command to launch
|
learn about the full Docker workflow for Garage.
|
||||||
Garage using Docker is provided in this quick start guide as well.
|
|
||||||
|
|
||||||
|
|
||||||
## Configuring and starting Garage
|
## Configuring and starting Garage
|
||||||
|
|
||||||
@@ -82,9 +80,6 @@ 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)"
|
||||||
@@ -95,10 +90,13 @@ 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.
|
||||||
|
|
||||||
Now that your configuration file has been created, you may save it to the directory of your choice.
|
By default, Garage looks for its configuration file in **`/etc/garage.toml`.**
|
||||||
By default, Garage looks for **`/etc/garage.toml`.**
|
Since we have written our configuration file in the working directory, we will have to set
|
||||||
You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
|
the following environment variable:
|
||||||
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`.
|
||||||
@@ -111,15 +109,41 @@ 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 -c path/to/garage.toml server
|
garage server --single-node --default-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have placed the `garage.toml` file in `/etc` (its default location), you can simply run `garage server`.
|
- the `--single-node` flag instructs Garage to automatically configure a
|
||||||
|
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:
|
||||||
@@ -127,21 +151,58 @@ you may use Docker to run Garage in a container using the following command:
|
|||||||
```bash
|
```bash
|
||||||
docker run \
|
docker run \
|
||||||
-d \
|
-d \
|
||||||
--name garaged \
|
--name garage-container \
|
||||||
-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 /path/to/garage.toml:/etc/garage.toml \
|
-v $(pwd)/garage.toml:/etc/garage.toml \
|
||||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
-e GARAGE_DEFAULT_ACCESS_KEY \
|
||||||
-v /path/to/garage/data:/var/lib/garage/data \
|
-e GARAGE_DEFAULT_SECRET_KEY \
|
||||||
dxflrs/garage:v2.2.0
|
-e GARAGE_DEFAULT_BUCKET \
|
||||||
|
dxflrs/garage:v2.3.0
|
||||||
|
/garage server --single-node --default-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
Note that this command will NOT create persistent volumes for Garage's data, so
|
||||||
|
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)).
|
||||||
|
|
||||||
#### Troubleshooting
|
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
You can tune Garage's verbosity by setting the `RUST_LOG=` environment variable. \
|
When running the `garage` CLI, ensure that the path to your configuration file is correctly specified (see below),
|
||||||
|
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
|
||||||
@@ -154,36 +215,135 @@ 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
|
|
||||||
|
|
||||||
The `garage` utility is also used as a CLI tool to configure your Garage deployment.
|
## Uploading and downloading from Garage
|
||||||
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.
|
|
||||||
|
|
||||||
If you are running Garage in a Docker container, you can set `alias garage="docker exec -ti <container name> /garage"`
|
This section will show how to download and upload files on Garage using a third-party tool named `awscli`.
|
||||||
to use the Garage binary inside your container.
|
|
||||||
|
|
||||||
If the `garage` CLI is able to correctly detect the parameters of your local Garage node,
|
|
||||||
the following command should be enough to show the status of your cluster:
|
|
||||||
|
|
||||||
```
|
### Install and configure `awscli`
|
||||||
garage status
|
|
||||||
|
If you have python on your system, you can install it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install --user awscli
|
||||||
```
|
```
|
||||||
|
|
||||||
This should show something like this:
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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 Tag Zone Capacity
|
ID Hostname Address Tags Zone Capacity DataAvail Version
|
||||||
563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED
|
563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED v2.3.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Creating a cluster layout
|
Creating a cluster layout for a Garage deployment means informing Garage of the
|
||||||
|
disk space available on each node of the cluster using the `-c` flag, as well
|
||||||
Creating a cluster layout for a Garage deployment means informing Garage
|
as the name of the zone (e.g. datacenter) each machine is located in using the
|
||||||
of the disk space available on each node of the cluster, `-c`,
|
`-z` flag.
|
||||||
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
|
||||||
@@ -204,38 +364,29 @@ 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.
|
main data storage. We will suppose that we want to create a bucket named
|
||||||
|
`nextcloud-bucket` that will be accessed through a key named
|
||||||
|
`nextcloud-app-key`.
|
||||||
|
|
||||||
First, create a bucket with the following command:
|
#### Create a bucket
|
||||||
|
|
||||||
|
First, create the bucket with the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
garage bucket create nextcloud-bucket
|
garage bucket create nextcloud-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
Check that everything went well:
|
Check that the bucket was created properly:
|
||||||
|
|
||||||
```
|
```
|
||||||
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.
|
||||||
@@ -258,14 +409,14 @@ Secret key: 7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34
|
|||||||
Authorized buckets:
|
Authorized buckets:
|
||||||
```
|
```
|
||||||
|
|
||||||
Check that everything works as intended:
|
Check that the key was created properly:
|
||||||
|
|
||||||
```
|
```
|
||||||
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:
|
||||||
|
|
||||||
@@ -284,78 +435,5 @@ 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
|
||||||
## Uploading and downloading from Garage
|
credentials created above.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ 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"
|
||||||
|
|
||||||
# for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
|
# optionally enable `token` for authentication:
|
||||||
# token = "abcdef-01234-56789"
|
# token = "abcdef-01234-56789"
|
||||||
|
|
||||||
tags = [ "dns-enabled" ]
|
tags = [ "dns-enabled" ]
|
||||||
@@ -174,6 +175,9 @@ 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:
|
||||||
|
|
||||||
@@ -447,7 +451,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 [`parse_duration`](https://docs.rs/parse_duration/latest/parse_duration/#syntax) crate.
|
the [`fundu_systemd`](https://docs.rs/fundu-systemd) 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
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ 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 currently disabled at compile time in all builds, as the
|
K2V is included in release builds since version 0.8.0. Precompiled builds
|
||||||
specification is still subject to changes. To build a Garage version with
|
of earlier versions including `k2v` can be found in our download page under
|
||||||
K2V, the Cargo feature flag `k2v` must be activated. Special builds with
|
"Extra builds": they can be easily identified as their tag name ends with
|
||||||
the `k2v` feature flag enabled can be obtained from our download page under
|
`-k2v` (example: `v0.7.2-k2v`). Otherwise, when compiling Garage, the Cargo
|
||||||
"Extra builds": such builds can be identified easily as their tag name ends
|
feature flag `k2v` must be activated.
|
||||||
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).
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
+++
|
||||||
|
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
|
||||||
|
```
|
||||||
@@ -166,6 +166,25 @@ 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
|
||||||
@@ -210,6 +229,12 @@ 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:
|
||||||
|
|
||||||
@@ -325,6 +350,12 @@ 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>`**
|
||||||
|
|
||||||
@@ -521,6 +552,14 @@ 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:
|
||||||
@@ -711,6 +750,14 @@ 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 |
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
*
|
||||||
|
|
||||||
|
!*.txt
|
||||||
|
!*.md
|
||||||
|
|
||||||
|
!assets
|
||||||
|
|
||||||
|
!.gitignore
|
||||||
|
!*.svg
|
||||||
|
!*.png
|
||||||
|
!*.jpg
|
||||||
|
!*.tex
|
||||||
|
!Makefile
|
||||||
|
!.gitignore
|
||||||
|
!assets/*.drawio.pdf
|
||||||
|
|
||||||
|
talk.{nav,out,snm,toc,aux,log}
|
||||||
|
!talk.pdf
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
talk.pdf: talk.tex
|
||||||
|
pdflatex talk.tex
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,330 @@
|
|||||||
|
%\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 :
|
||||||
@@ -81,17 +81,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763952169,
|
"lastModified": 1776914043,
|
||||||
"narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=",
|
"narHash": "sha256-qug5r56yW1qOsjSI99l3Jm15JNT9CvS2otkXNRNtrPI=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
"rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
"rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
inputs.nixpkgs.url =
|
inputs.nixpkgs.url =
|
||||||
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
|
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
|
||||||
|
|
||||||
# Rust overlay as of 2025-11-24
|
# Rust overlay as of 2026-04-23
|
||||||
inputs.rust-overlay.url =
|
inputs.rust-overlay.url =
|
||||||
"github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b";
|
"github:oxalica/rust-overlay/2d35c4358d7de3a0e606a6e8b27925d981c01cc3";
|
||||||
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,6 +95,14 @@
|
|||||||
killall
|
killall
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# dev shell for fuzzing
|
||||||
|
fuzz = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
targets.toolchainNightly
|
||||||
|
cargo-fuzz
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
||||||
|
coverage
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#![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),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#![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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#![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),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ let
|
|||||||
|
|
||||||
inherit (pkgs) lib stdenv;
|
inherit (pkgs) lib stdenv;
|
||||||
|
|
||||||
toolchainFn = (p: p.rust-bin.stable."1.91.0".default.override {
|
toolchainFn = (p: p.rust-bin.stable."1.95.0".default.override {
|
||||||
targets = lib.optionals (target != null) [ rustTarget ];
|
targets = lib.optionals (target != null) [ rustTarget ];
|
||||||
extensions = [
|
extensions = [
|
||||||
"rust-src"
|
"rust-src"
|
||||||
@@ -148,6 +148,14 @@ 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
|
||||||
|
|||||||
@@ -21,3 +21,5 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.tmproj
|
*.tmproj
|
||||||
.vscode/
|
.vscode/
|
||||||
|
# helm-unittest test suites
|
||||||
|
tests/
|
||||||
|
|||||||
@@ -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.2
|
version: 0.9.4
|
||||||
appVersion: "v2.2.0"
|
appVersion: "v2.3.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,4 +15,6 @@ 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,6 +1,6 @@
|
|||||||
# garage
|
# garage
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
S3-compatible object store for small self-hosted geo-distributed deployments
|
||||||
|
|
||||||
@@ -33,11 +33,14 @@ 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 |
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# 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,6 +71,13 @@ 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,9 +5,11 @@ 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"]
|
||||||
|
|||||||
@@ -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 = "[::]:3900"
|
api_bind_addr = "{{ .Values.garage.s3.api.bindAddr }}"
|
||||||
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
|
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
|
||||||
|
|
||||||
[s3_web]
|
[s3_web]
|
||||||
bind_addr = "[::]:3902"
|
bind_addr = "{{ .Values.garage.s3.web.bindAddr }}"
|
||||||
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 = "[::]:3903"
|
api_bind_addr = "{{ .Values.garage.admin.apiBindAddr }}"
|
||||||
{{- 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,7 +62,9 @@ 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: 3900
|
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: s3-api
|
name: s3-api
|
||||||
- port: {{ .Values.service.s3.web.port }}
|
- port: {{ .Values.service.s3.web.port }}
|
||||||
targetPort: 3902
|
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: s3-web
|
name: s3-web
|
||||||
selector:
|
selector:
|
||||||
|
|||||||
@@ -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: 3900
|
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: s3-api
|
name: s3-api
|
||||||
- port: {{ .Values.service.s3.web.port }}
|
- port: {{ .Values.service.s3.web.port }}
|
||||||
targetPort: 3902
|
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
|
||||||
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: 3903
|
- port: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
|
||||||
targetPort: 3903
|
targetPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
|
||||||
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 6 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
|
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
|
||||||
relabelings:
|
relabelings:
|
||||||
{{- toYaml . | nindent 6 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
jobLabel: "{{ .Release.Name }}"
|
jobLabel: "{{ .Release.Name }}"
|
||||||
selector:
|
selector:
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ 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:
|
||||||
@@ -57,11 +60,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: 3900
|
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
|
||||||
name: s3-api
|
name: s3-api
|
||||||
- containerPort: 3902
|
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
|
||||||
name: web-api
|
name: web-api
|
||||||
- containerPort: 3903
|
- containerPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
|
||||||
name: admin
|
name: admin
|
||||||
{{- with .Values.environment }}
|
{{- with .Values.environment }}
|
||||||
env:
|
env:
|
||||||
@@ -91,7 +94,7 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: configmap
|
- name: configmap
|
||||||
configMap:
|
configMap:
|
||||||
name: {{ include "garage.fullname" . }}-config
|
name: {{ if .Values.garage.existingConfigMap }}{{ .Values.garage.existingConfigMap }}{{ else }}{{ include "garage.fullname" . }}-config{{ end }}
|
||||||
- name: etc
|
- name: etc
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
{{- if .Values.persistence.enabled }}
|
{{- if .Values.persistence.enabled }}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 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"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 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"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Enable Prometheus metrics scraping and a ServiceMonitor for the prometheus-operator.
|
||||||
|
monitoring:
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
interval: 30s
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
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
|
||||||
@@ -48,11 +48,15 @@ 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:
|
||||||
@@ -221,14 +225,14 @@ resources: {}
|
|||||||
livenessProbe: {}
|
livenessProbe: {}
|
||||||
#httpGet:
|
#httpGet:
|
||||||
# path: /health
|
# path: /health
|
||||||
# port: 3903
|
# port: 3903 # or the port from garage.admin.apiBindAddr
|
||||||
#initialDelaySeconds: 5
|
#initialDelaySeconds: 5
|
||||||
#periodSeconds: 30
|
#periodSeconds: 30
|
||||||
# -- Specifies a readinessProbe
|
# -- Specifies a readinessProbe
|
||||||
readinessProbe: {}
|
readinessProbe: {}
|
||||||
#httpGet:
|
#httpGet:
|
||||||
# path: /health
|
# path: /health
|
||||||
# port: 3903
|
# port: 3903 # or the port from garage.admin.apiBindAddr
|
||||||
#initialDelaySeconds: 5
|
#initialDelaySeconds: 5
|
||||||
#periodSeconds: 30
|
#periodSeconds: 30
|
||||||
|
|
||||||
@@ -238,10 +242,18 @@ 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,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_admin"
|
name = "garage_api_admin"
|
||||||
version = "2.2.0"
|
version = "2.3.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,3 +48,6 @@ 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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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::*;
|
||||||
@@ -244,8 +245,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().map(|x| {
|
expiration: params.expiration.get().inner().map(|x| {
|
||||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
DateTime::from_timestamp_millis(x.0 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(),
|
||||||
@@ -279,10 +280,10 @@ fn apply_token_updates(
|
|||||||
if let Some(expiration) = updates.expiration {
|
if let Some(expiration) = updates.expiration {
|
||||||
params
|
params
|
||||||
.expiration
|
.expiration
|
||||||
.update(Some(expiration.timestamp_millis() as u64));
|
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
|
||||||
}
|
}
|
||||||
if updates.never_expires {
|
if updates.never_expires {
|
||||||
params.expiration.update(None);
|
params.expiration.update(None.into());
|
||||||
}
|
}
|
||||||
if let Some(scope) = updates.scope {
|
if let Some(scope) = updates.scope {
|
||||||
params.scope.update(AdminApiTokenScope(scope));
|
params.scope.update(AdminApiTokenScope(scope));
|
||||||
|
|||||||
@@ -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};
|
use garage_api_common::{common_error::CommonError, helpers::is_default, xml};
|
||||||
|
|
||||||
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,8 +282,34 @@ 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 ----
|
||||||
@@ -592,6 +618,10 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -613,6 +643,10 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -843,9 +877,16 @@ 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
|
||||||
@@ -869,6 +910,9 @@ 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)]
|
||||||
@@ -927,6 +971,11 @@ 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)]
|
||||||
@@ -935,6 +984,9 @@ 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 ----
|
||||||
@@ -1109,10 +1161,41 @@ 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 ----
|
||||||
@@ -1121,8 +1204,47 @@ 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 ----
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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::*;
|
||||||
@@ -37,7 +38,7 @@ impl RequestHandler for ListBucketsRequest {
|
|||||||
&EmptyKey,
|
&EmptyKey,
|
||||||
None,
|
None,
|
||||||
Some(DeletedFilter::NotDeleted),
|
Some(DeletedFilter::NotDeleted),
|
||||||
10000,
|
1_000_000,
|
||||||
EnumerationOrder::Forward,
|
EnumerationOrder::Forward,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -89,7 +90,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())
|
.and_then(|x| x.state.get().into_inner())
|
||||||
.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();
|
||||||
@@ -167,7 +168,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().is_some() {
|
if alias.state.get().inner().is_some() {
|
||||||
return Err(CommonError::BucketAlreadyExists.into());
|
return Err(CommonError::BucketAlreadyExists.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,25 +294,46 @@ 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, routing_rules) = match state.website_config.get() {
|
let redirect_all = state
|
||||||
Some(wc) => (wc.redirect_all.clone(), wc.routing_rules.clone()),
|
.website_config
|
||||||
None => (None, Vec::new()),
|
.get()
|
||||||
|
.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);
|
state.website_config.update(None.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +344,38 @@ 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(
|
||||||
@@ -557,7 +611,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,
|
||||||
@@ -565,7 +619,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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +645,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,
|
||||||
@@ -599,7 +653,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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,45 +690,36 @@ 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 mut relevant_keys = HashMap::new();
|
let state = bucket.state.as_option().unwrap();
|
||||||
for (k, _) in bucket
|
|
||||||
.state
|
let keys1 = state
|
||||||
.as_option()
|
|
||||||
.unwrap()
|
|
||||||
.authorized_keys
|
.authorized_keys
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
{
|
.filter(|(_, p)| p.is_any())
|
||||||
if let Some(key) = garage
|
.map(|(k, _)| k);
|
||||||
.key_table
|
let keys2 = state
|
||||||
.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)
|
||||||
if relevant_keys.contains_key(k) {
|
.map(|((k, _), _, _)| 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, k).await? {
|
if let Some(key) = garage.key_table.get(&EmptyKey, key_id).await? {
|
||||||
if !key.state.is_deleted() {
|
relevant_keys.insert(key_id.clone(), key);
|
||||||
relevant_keys.insert(k.clone(), key);
|
} else {
|
||||||
|
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 {
|
||||||
@@ -688,38 +733,56 @@ 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().is_some(),
|
website_access: state.website_config.get().inner().is_some(),
|
||||||
website_config: state.website_config.get().clone().map(|wsc| {
|
website_config: state.website_config.get().inner().cloned().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()
|
||||||
.filter_map(|key| {
|
.map(|key| {
|
||||||
let p = key.state.as_option().unwrap();
|
let st = key.state.as_option().unwrap();
|
||||||
let permissions = p
|
let permissions = st
|
||||||
.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,
|
||||||
})?;
|
})
|
||||||
Some(GetBucketInfoKey {
|
.unwrap_or_default();
|
||||||
access_key_id: key.key_id,
|
let bucket_local_aliases = st
|
||||||
name: p.name.get().to_string(),
|
|
||||||
permissions,
|
|
||||||
bucket_local_aliases: p
|
|
||||||
.local_aliases
|
.local_aliases
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, _, b)| *b == Some(bucket.id))
|
.filter(|(_, _, b)| b.into_inner() == 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),
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ 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::*;
|
||||||
@@ -152,7 +154,6 @@ 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>,
|
||||||
@@ -160,8 +161,60 @@ impl RequestHandler for GetClusterStatisticsRequest {
|
|||||||
) -> Result<GetClusterStatisticsResponse, Error> {
|
) -> Result<GetClusterStatisticsResponse, Error> {
|
||||||
let mut ret = String::new();
|
let mut ret = String::new();
|
||||||
|
|
||||||
// Gather storage node and free space statistics for current nodes
|
// 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 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
|
||||||
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() {
|
||||||
@@ -231,33 +284,57 @@ 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 meta_avail =
|
let metadata_avail: u64 =
|
||||||
bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
|
meta_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS);
|
||||||
let data_avail =
|
let data_avail: u64 = data_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS);
|
||||||
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,
|
||||||
"\nEstimated available storage space cluster-wide (might be lower in practice):"
|
"Estimated available storage space cluster-wide (might be lower in practice):"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if meta_part_avail.len() < node_partition_count.len()
|
if incomplete_info {
|
||||||
|| data_part_avail.len() < node_partition_count.len()
|
|
||||||
{
|
|
||||||
ret += &format_table_to_string(vec![
|
ret += &format_table_to_string(vec![
|
||||||
format!(" data: < {}", data_avail),
|
format!(" data: < {}", data_avail_str),
|
||||||
format!(" metadata: < {}", meta_avail),
|
format!(" metadata: < {}", metadata_avail_str),
|
||||||
]);
|
]);
|
||||||
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),
|
format!(" data: {}", data_avail_str),
|
||||||
format!(" metadata: {}", meta_avail),
|
format!(" metadata: {}", metadata_avail_str),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GetClusterStatisticsResponse { freeform: ret })
|
Ok(GetClusterStatisticsResponse {
|
||||||
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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::*;
|
||||||
@@ -40,8 +41,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().map(|x| {
|
expiration: p.expiration.get().inner().map(|x| {
|
||||||
DateTime::from_timestamp_millis(x as i64)
|
DateTime::from_timestamp_millis(x.0 as i64)
|
||||||
.expect("invalid timestamp stored in db")
|
.expect("invalid timestamp stored in db")
|
||||||
}),
|
}),
|
||||||
expired: p.is_expired(now),
|
expired: p.is_expired(now),
|
||||||
@@ -76,7 +77,9 @@ impl RequestHandler for GetKeyInfoRequest {
|
|||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if candidates.len() != 1 {
|
if candidates.is_empty() {
|
||||||
|
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()
|
||||||
@@ -185,38 +188,42 @@ 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();
|
||||||
|
|
||||||
for id in key_state
|
let buckets1 = key_state
|
||||||
.authorized_buckets
|
.authorized_buckets
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, _)| id)
|
.filter(|(_, p)| p.is_any())
|
||||||
.chain(
|
.map(|(id, _)| id);
|
||||||
key_state
|
let buckets2 = key_state
|
||||||
.local_aliases
|
.local_aliases
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, _, v)| v.as_ref()),
|
.filter_map(|(_, _, v)| v.inner());
|
||||||
) {
|
|
||||||
if !relevant_buckets.contains_key(id) {
|
let mut relevant_buckets = HashMap::new();
|
||||||
if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? {
|
for bucket_id in buckets1.chain(buckets2) {
|
||||||
if b.state.as_option().is_some() {
|
if !relevant_buckets.contains_key(bucket_id) {
|
||||||
relevant_buckets.insert(*id, b);
|
if let Some(b) = garage.bucket_table.get(&EmptyKey, bucket_id).await? {
|
||||||
}
|
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().map(|x| {
|
expiration: key_state.expiration.get().inner().map(|x| {
|
||||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
DateTime::from_timestamp_millis(x.0 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(),
|
||||||
@@ -230,7 +237,7 @@ async fn key_info_results(
|
|||||||
},
|
},
|
||||||
buckets: relevant_buckets
|
buckets: relevant_buckets
|
||||||
.into_values()
|
.into_values()
|
||||||
.filter_map(|bucket| {
|
.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
|
||||||
@@ -240,8 +247,9 @@ 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,
|
||||||
})?;
|
})
|
||||||
Some(KeyInfoBucketResponse {
|
.unwrap_or_default();
|
||||||
|
KeyInfoBucketResponse {
|
||||||
id: hex::encode(bucket.id),
|
id: hex::encode(bucket.id),
|
||||||
global_aliases: state
|
global_aliases: state
|
||||||
.aliases
|
.aliases
|
||||||
@@ -258,7 +266,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<_>>(),
|
||||||
};
|
};
|
||||||
@@ -281,10 +289,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(expiration.timestamp_millis() as u64));
|
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
|
||||||
}
|
}
|
||||||
if updates.never_expires {
|
if updates.never_expires {
|
||||||
key_state.expiration.update(None);
|
key_state.expiration.update(None.into());
|
||||||
}
|
}
|
||||||
if let Some(allow) = updates.allow {
|
if let Some(allow) = updates.allow {
|
||||||
if allow.create_bucket {
|
if allow.create_bucket {
|
||||||
|
|||||||
@@ -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) != Some(v))
|
.filter(|(k, _, v)| current.roles.get(k).and_then(|vv| vv.0.as_ref()) != v.0.as_ref())
|
||||||
.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,10 +255,14 @@ 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, msg)) => Ok(PreviewClusterLayoutChangesResponse::Success {
|
Ok((new_layout, stat)) => {
|
||||||
message: msg,
|
let message = stat.to_message();
|
||||||
|
Ok(PreviewClusterLayoutChangesResponse::Success {
|
||||||
|
message,
|
||||||
|
statistics: Some(Box::new(stat)),
|
||||||
new_layout: format_cluster_layout(&new_layout),
|
new_layout: format_cluster_layout(&new_layout),
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,7 +276,8 @@ 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, msg) = layout.apply_staged_changes(self.version)?;
|
let (layout, stat) = layout.apply_staged_changes(self.version)?;
|
||||||
|
let message = stat.to_message();
|
||||||
|
|
||||||
garage
|
garage
|
||||||
.system
|
.system
|
||||||
@@ -281,7 +286,8 @@ impl RequestHandler for ApplyClusterLayoutRequest {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(ApplyClusterLayoutResponse {
|
Ok(ApplyClusterLayoutResponse {
|
||||||
message: msg,
|
message,
|
||||||
|
statistics: Some(stat),
|
||||||
layout: format_cluster_layout(&layout),
|
layout: format_cluster_layout(&layout),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,55 @@ 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,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,46 +99,58 @@ 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{}", sys_status.hostname.unwrap_or_default(),),
|
format!("Hostname:\t{}", hostname),
|
||||||
format!(
|
format!("Garage version:\t{}", garage_version),
|
||||||
"Garage version:\t{}",
|
format!("Garage features:\t{}", garage_features.join(", ")),
|
||||||
garage_util::version::garage_version(),
|
format!("Rust compiler version:\t{}", rustc_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()),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Gather table statistics
|
let mut table_stats = vec![
|
||||||
let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()];
|
gather_table_stats(&garage.admin_token_table)?,
|
||||||
table.push(gather_table_stats(&garage.admin_token_table)?);
|
gather_table_stats(&garage.bucket_table)?,
|
||||||
table.push(gather_table_stats(&garage.bucket_table)?);
|
gather_table_stats(&garage.bucket_alias_table)?,
|
||||||
table.push(gather_table_stats(&garage.bucket_alias_table)?);
|
gather_table_stats(&garage.key_table)?,
|
||||||
table.push(gather_table_stats(&garage.key_table)?);
|
gather_table_stats(&garage.object_table)?,
|
||||||
|
gather_table_stats(&garage.object_counter_table.table)?,
|
||||||
table.push(gather_table_stats(&garage.object_table)?);
|
gather_table_stats(&garage.mpu_table)?,
|
||||||
table.push(gather_table_stats(&garage.object_counter_table.table)?);
|
gather_table_stats(&garage.mpu_counter_table.table)?,
|
||||||
table.push(gather_table_stats(&garage.mpu_table)?);
|
gather_table_stats(&garage.version_table)?,
|
||||||
table.push(gather_table_stats(&garage.mpu_counter_table.table)?);
|
gather_table_stats(&garage.block_ref_table)?,
|
||||||
table.push(gather_table_stats(&garage.version_table)?);
|
];
|
||||||
table.push(gather_table_stats(&garage.block_ref_table)?);
|
|
||||||
|
|
||||||
#[cfg(feature = "k2v")]
|
#[cfg(feature = "k2v")]
|
||||||
{
|
{
|
||||||
table.push(gather_table_stats(&garage.k2v.item_table)?);
|
table_stats.push(gather_table_stats(&garage.k2v.item_table)?);
|
||||||
table.push(gather_table_stats(&garage.k2v.counter_table.table)?);
|
table_stats.push(gather_table_stats(&garage.k2v.counter_table.table)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gather table statistics
|
||||||
|
let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()];
|
||||||
|
table.extend(table_stats.iter().map(|ts| {
|
||||||
|
format!(
|
||||||
|
" {}\t{}\t{}\t{}\t{}\t{}",
|
||||||
|
ts.table_name,
|
||||||
|
ts.items,
|
||||||
|
ts.merkle_items,
|
||||||
|
ts.merkle_queue_len,
|
||||||
|
ts.insert_queue_len,
|
||||||
|
ts.gc_queue_len,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
"\nTable stats:\n{}",
|
"\nTable stats:\n{}",
|
||||||
@@ -104,46 +158,52 @@ 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!(" number of RC entries:\t{} (~= number of blocks)", rc_len),
|
format!(
|
||||||
|
" number of RC entries:\t{} (~= number of blocks)",
|
||||||
|
block_manager_stats.rc_entries
|
||||||
|
),
|
||||||
format!(
|
format!(
|
||||||
" resync queue length:\t{}",
|
" resync queue length:\t{}",
|
||||||
garage.block_manager.resync.queue_approximate_len()?
|
block_manager_stats.resync_queue_len,
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
" blocks with resync errors:\t{}",
|
" blocks with resync errors:\t{}",
|
||||||
garage.block_manager.resync.errors_approximate_len()?
|
block_manager_stats.resync_errors
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Ok(LocalGetNodeStatisticsResponse { freeform: ret })
|
Ok(LocalGetNodeStatisticsResponse {
|
||||||
|
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<String, Error>
|
fn gather_table_stats<F, R>(t: &Arc<Table<F, R>>) -> Result<NodeTableStats, Error>
|
||||||
where
|
where
|
||||||
F: TableSchema + 'static,
|
F: TableSchema + 'static,
|
||||||
R: TableReplication + 'static,
|
R: TableReplication + 'static,
|
||||||
{
|
{
|
||||||
let data_len = t
|
let data_len = t.data.store.approximate_len().map_err(GarageError::from)?;
|
||||||
.data
|
let mkl_len = t.merkle_updater.merkle_tree_approximate_len()?;
|
||||||
.store
|
|
||||||
.approximate_len()
|
|
||||||
.map_err(GarageError::from)?
|
|
||||||
.to_string();
|
|
||||||
let mkl_len = t.merkle_updater.merkle_tree_approximate_len()?.to_string();
|
|
||||||
|
|
||||||
Ok(format!(
|
Ok(NodeTableStats {
|
||||||
" {}\t{}\t{}\t{}\t{}\t{}",
|
table_name: F::TABLE_NAME.to_string(),
|
||||||
F::TABLE_NAME,
|
items: data_len as u64,
|
||||||
data_len,
|
merkle_items: mkl_len as u64,
|
||||||
mkl_len,
|
merkle_queue_len: t.merkle_updater.todo_approximate_len()? as u64,
|
||||||
t.merkle_updater.todo_approximate_len()?,
|
insert_queue_len: t.data.insert_queue_approximate_len()? as u64,
|
||||||
t.data.insert_queue_approximate_len()?,
|
gc_queue_len: t.data.gc_todo_approximate_len()? as u64,
|
||||||
t.data.gc_todo_approximate_len()?
|
})
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.2.0",
|
version = "v2.3.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.
|
||||||
|
|
||||||
|
|||||||
@@ -360,14 +360,7 @@ 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
|
let next1 = self.block_manager.rc.get_first_hash_from(self.cursor)?;
|
||||||
.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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
let bucket_website_config = bucket_state.website_config.get().inner();
|
||||||
|
|
||||||
match bucket_website_config {
|
match bucket_website_config {
|
||||||
Some(_v) => Ok(true),
|
Some(_v) => Ok(true),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_common"
|
name = "garage_api_common"
|
||||||
version = "2.2.0"
|
version = "2.3.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,6 +27,7 @@ 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
|
||||||
@@ -41,7 +42,12 @@ 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
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ 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
|
||||||
@@ -55,6 +59,10 @@ 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]
|
||||||
@@ -97,8 +105,11 @@ 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::BucketAlreadyExists => StatusCode::CONFLICT,
|
CommonError::BucketNotEmpty
|
||||||
|
| CommonError::BucketAlreadyExists
|
||||||
|
| CommonError::BucketAlreadyOwnedByYou => StatusCode::CONFLICT,
|
||||||
CommonError::InvalidBucketName(_) | CommonError::InvalidHeader(_) => {
|
CommonError::InvalidBucketName(_) | CommonError::InvalidHeader(_) => {
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
}
|
}
|
||||||
@@ -120,6 +131,8 @@ 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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +155,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) {
|
||||||
|
|||||||