mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-09-05 19:55:39 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be17e25bee | |||
| 8d85301808 |
@@ -53,15 +53,3 @@ steps:
|
|||||||
- nix-build -j4 --attr flakePackages.dev
|
- nix-build -j4 --attr flakePackages.dev
|
||||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||||
depends_on: [ build ]
|
depends_on: [ build ]
|
||||||
|
|
||||||
- name: helm chart tests
|
|
||||||
image: helmunittest/helm-unittest:4.2.3-1.1.2
|
|
||||||
commands:
|
|
||||||
- helm lint --strict script/helm/garage
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/daemonset.yaml
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/ingress.yaml
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/existing-secret.yaml
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/monitoring.yaml
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/minimal.yaml
|
|
||||||
- helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
|
|
||||||
- helm unittest --strict script/helm/garage
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"name": "AGPL-3.0",
|
"name": "AGPL-3.0",
|
||||||
"identifier": "AGPL-3.0"
|
"identifier": "AGPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "v2.3.0"
|
"version": "v2.4.0"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@@ -1243,6 +1243,36 @@
|
|||||||
],
|
],
|
||||||
"description": "List all the buckets on the cluster with their UUID and their global and local aliases.",
|
"description": "List all the buckets on the cluster with their UUID and their global and local aliases.",
|
||||||
"operationId": "ListBuckets",
|
"operationId": "ListBuckets",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "details",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Returned detailed informations in the same format as GetBucketInfo for each bucket",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Bucket ID of the first bucket to return",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Maximum number of buckets to return in a single call",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Returns the UUID of all the buckets and all their aliases",
|
"description": "Returns the UUID of all the buckets and all their aliases",
|
||||||
@@ -1267,6 +1297,36 @@
|
|||||||
],
|
],
|
||||||
"description": "Returns all API access keys in the cluster.",
|
"description": "Returns all API access keys in the cluster.",
|
||||||
"operationId": "ListKeys",
|
"operationId": "ListKeys",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "details",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Returned detailed informations in the same format as GetKeyInfo for each bucket",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Key ID of the first key to return",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Maximum number of keys to return in a single call",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)",
|
"description": "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)",
|
||||||
@@ -3200,10 +3260,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ListBucketsResponse": {
|
"ListBucketsResponse": {
|
||||||
"type": "array",
|
"oneOf": [
|
||||||
"items": {
|
{
|
||||||
"$ref": "#/components/schemas/ListBucketsResponseItem"
|
"type": "array",
|
||||||
}
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ListBucketsResponseItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ListBucketsResponseItem": {
|
"ListBucketsResponseItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -3236,10 +3306,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ListKeysResponse": {
|
"ListKeysResponse": {
|
||||||
"type": "array",
|
"oneOf": [
|
||||||
"items": {
|
{
|
||||||
"$ref": "#/components/schemas/ListKeysResponseItem"
|
"type": "array",
|
||||||
}
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ListKeysResponseItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GetKeyInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ListKeysResponseItem": {
|
"ListKeysResponseItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -3321,10 +3401,35 @@
|
|||||||
"dbEngine"
|
"dbEngine"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"addr": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Socket address used by other nodes to connect to this node for RPC"
|
||||||
|
},
|
||||||
|
"dataPartition": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/FreeSpaceResp",
|
||||||
|
"description": "Total and available space on the disk partition(s) containing the data\ndirectory(ies)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dbEngine": {
|
"dbEngine": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "database engine used for metadata"
|
"description": "database engine used for metadata"
|
||||||
},
|
},
|
||||||
|
"draining": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Whether this node is part of an older layout version and is draining data."
|
||||||
|
},
|
||||||
"garageFeatures": {
|
"garageFeatures": {
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
@@ -3346,9 +3451,38 @@
|
|||||||
],
|
],
|
||||||
"description": "hostname of this node"
|
"description": "hostname of this node"
|
||||||
},
|
},
|
||||||
|
"isUp": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Whether this node is connected in the cluster"
|
||||||
|
},
|
||||||
|
"metadataPartition": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/FreeSpaceResp",
|
||||||
|
"description": "Total and available space on the disk partition containing the\nmetadata directory"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"nodeId": {
|
"nodeId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"role": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NodeAssignedRole",
|
||||||
|
"description": "Role assigned to this node in the current cluster layout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"rustVersion": {
|
"rustVersion": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "rustc version with which this garage release was compiled"
|
"description": "rustc version with which this garage release was compiled"
|
||||||
@@ -3684,10 +3818,35 @@
|
|||||||
"dbEngine"
|
"dbEngine"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"addr": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Socket address used by other nodes to connect to this node for RPC"
|
||||||
|
},
|
||||||
|
"dataPartition": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/FreeSpaceResp",
|
||||||
|
"description": "Total and available space on the disk partition(s) containing the data\ndirectory(ies)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dbEngine": {
|
"dbEngine": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "database engine used for metadata"
|
"description": "database engine used for metadata"
|
||||||
},
|
},
|
||||||
|
"draining": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Whether this node is part of an older layout version and is draining data."
|
||||||
|
},
|
||||||
"garageFeatures": {
|
"garageFeatures": {
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
@@ -3709,9 +3868,38 @@
|
|||||||
],
|
],
|
||||||
"description": "hostname of this node"
|
"description": "hostname of this node"
|
||||||
},
|
},
|
||||||
|
"isUp": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Whether this node is connected in the cluster"
|
||||||
|
},
|
||||||
|
"metadataPartition": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/FreeSpaceResp",
|
||||||
|
"description": "Total and available space on the disk partition containing the\nmetadata directory"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"nodeId": {
|
"nodeId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"role": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NodeAssignedRole",
|
||||||
|
"description": "Role assigned to this node in the current cluster layout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"rustVersion": {
|
"rustVersion": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "rustc version with which this garage release was compiled"
|
"description": "rustc version with which this garage release was compiled"
|
||||||
|
|||||||
@@ -133,17 +133,12 @@ Use the following command to launch the Garage server:
|
|||||||
garage server --single-node --default-bucket
|
garage server --single-node --default-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
- the `--single-node` flag instructs Garage to automatically configure a
|
The `--single-node` flag instructs Garage to automatically configure a single-node cluster without data replication.
|
||||||
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.
|
||||||
- the `--default-bucket` flag instructs Garage to create a default access key
|
Both flags are optional and can be omitted, in which case you will have to follow manual configuration steps described below.
|
||||||
and a default bucket using the environment variables we defined above (it
|
|
||||||
implies `--default-access-key`).
|
|
||||||
|
|
||||||
> You can refer to the [manual configuration
|
**For older versions of Garage (before v2.3.0):** automatic configuration using `--single-node` and `--default-bucket` is not available,
|
||||||
> steps](#manual-configuration) if:
|
you must follow the manual configuration steps.
|
||||||
>
|
|
||||||
> - 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:
|
||||||
@@ -297,7 +292,7 @@ An exhaustive list is maintained in the ["Integrations" > "Browsing tools" secti
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Manual configuration {#manual-configuration}
|
## Manual configuration
|
||||||
|
|
||||||
This section provides instructions that are equivalent to using the
|
This section provides instructions that are equivalent to using the
|
||||||
`--single-node` and `--default-bucket` flags for automatic configuration. If
|
`--single-node` and `--default-bucket` flags for automatic configuration. If
|
||||||
|
|||||||
@@ -175,9 +175,6 @@ they do not exist in the configuration file:
|
|||||||
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
||||||
instead of printing to stderr.
|
instead of printing to stderr.
|
||||||
|
|
||||||
- `NO_COLOR` (since `v2.4.0`): set this to `0` or `false` to disable
|
|
||||||
ANSI color codes in Garage's logs.
|
|
||||||
|
|
||||||
The following environment variables can be used to override the corresponding
|
The following environment variables can be used to override the corresponding
|
||||||
values in the configuration file:
|
values in the configuration file:
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ which is an alternative storage API designed to help efficiently store
|
|||||||
many small values in buckets (in opposition to S3 which is more designed
|
many small values in buckets (in opposition to S3 which is more designed
|
||||||
to store large blobs).
|
to store large blobs).
|
||||||
|
|
||||||
K2V is included in release builds since version 0.8.0. Precompiled builds
|
K2V is currently disabled at compile time in all builds, as the
|
||||||
of earlier versions including `k2v` can be found in our download page under
|
specification is still subject to changes. To build a Garage version with
|
||||||
"Extra builds": they can be easily identified as their tag name ends with
|
K2V, the Cargo feature flag `k2v` must be activated. Special builds with
|
||||||
`-k2v` (example: `v0.7.2-k2v`). Otherwise, when compiling Garage, the Cargo
|
the `k2v` feature flag enabled can be obtained from our download page under
|
||||||
feature flag `k2v` must be activated.
|
"Extra builds": such builds can be identified easily as their tag name ends
|
||||||
|
with `-k2v` (example: `v0.7.2-k2v`).
|
||||||
|
|
||||||
The specification of the K2V API can be found
|
The specification of the K2V API can be found
|
||||||
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
|
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fi
|
|||||||
|
|
||||||
$GARAGE_BIN -c /tmp/config.1.toml bucket create eprouvette
|
$GARAGE_BIN -c /tmp/config.1.toml bucket create eprouvette
|
||||||
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
||||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key new --name opérateur)
|
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key create opérateur)
|
||||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||||
elif [ "$GARAGE_OLDVER" = "v1" ]; then
|
elif [ "$GARAGE_OLDVER" = "v1" ]; then
|
||||||
|
|||||||
@@ -21,5 +21,3 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.tmproj
|
*.tmproj
|
||||||
.vscode/
|
.vscode/
|
||||||
# helm-unittest test suites
|
|
||||||
tests/
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ apiVersion: v2
|
|||||||
name: garage
|
name: garage
|
||||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||||
type: application
|
type: application
|
||||||
version: 0.9.4
|
version: 0.9.3
|
||||||
appVersion: "v2.3.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,6 +15,4 @@ keywords:
|
|||||||
sources:
|
sources:
|
||||||
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||||
|
|
||||||
maintainers:
|
maintainers: []
|
||||||
- name: Garage maintainer team
|
|
||||||
email: garagehq@deuxfleurs.fr
|
|
||||||
|
|||||||
@@ -1,331 +0,0 @@
|
|||||||
# An "everything and the kitchen sink" values file for the helm chart: combines many non-default
|
|
||||||
# settings at once, including examples for the fields that default to empty in
|
|
||||||
# values.yaml and are therefore hard to guess the expected shape of.
|
|
||||||
#
|
|
||||||
# Aside the documentation value, it doubles as an integration-test fixture:
|
|
||||||
# CI renders and lints the chart with this file (see .woodpecker/debug.yaml)
|
|
||||||
# to catch feature interactions that per-feature fixtures wouldn't exercise together
|
|
||||||
# (e.g. both ingresses enabled at once, monitoring + custom service account,
|
|
||||||
# a DaemonSet-incompatible field set alongside a StatefulSet, ...).
|
|
||||||
#
|
|
||||||
# Try it locally with:
|
|
||||||
# helm template script/helm/garage -f script/helm/garage/complex-values.yaml
|
|
||||||
# helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
|
|
||||||
|
|
||||||
# -- Additional labels to add to all resources created by this chart
|
|
||||||
commonLabels:
|
|
||||||
app.kubernetes.io/part-of: storage
|
|
||||||
team: platform-infrastructure
|
|
||||||
|
|
||||||
# Garage configuration. Values under this are written to garage.toml
|
|
||||||
garage:
|
|
||||||
# -- sqlite for durability, lmdb for performance
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine
|
|
||||||
dbEngine: "sqlite"
|
|
||||||
|
|
||||||
# -- Here set to 10MiB
|
|
||||||
# An increase can result in better performance in certain scenarios
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size
|
|
||||||
blockSize: "10485760"
|
|
||||||
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor
|
|
||||||
replicationFactor: "5"
|
|
||||||
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode
|
|
||||||
consistencyMode: "dangerous"
|
|
||||||
|
|
||||||
# -- zstd compression level of stored blocks
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level
|
|
||||||
compressionLevel: "5"
|
|
||||||
|
|
||||||
# -- If this value is set, Garage will automatically take a snapshot of the metadata DB file and save it in the metadata directory.
|
|
||||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval
|
|
||||||
metadataAutoSnapshotInterval: "30 days"
|
|
||||||
|
|
||||||
rpcBindAddr: "[::]:3901"
|
|
||||||
# -- If not given, a random secret will be generated and stored in a Secret object
|
|
||||||
rpcSecret: ""
|
|
||||||
# -- If you want to provide an rpcSecret within an existing k8s secret,
|
|
||||||
# specify the secret name here, and store the value under the secret key `rpcSecret`
|
|
||||||
# ! the default secret will not be created
|
|
||||||
existingRpcSecret: ""
|
|
||||||
# -- This is not required if you use the integrated kubernetes discovery. Each
|
|
||||||
# entry is "<garage_node_id>@<host>:<port>", where <garage_node_id> is the node's public key
|
|
||||||
# (shown by `garage node id` on that node).
|
|
||||||
bootstrapPeers:
|
|
||||||
- "563e1ac825ee3323aa441e72c26d1030d6d4222c43c986812dbf7cd47d18aef@garage-0.garage-headless:3901"
|
|
||||||
- "86f0f26ae4afbd59aaf9cfb302af3fe0464f2f7b5b21f80f7e6f4e9989b5c1f8@garage-1.garage-headless:3901"
|
|
||||||
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
|
|
||||||
# of the helm chart, for example if you operate at namespace level without cluster resources
|
|
||||||
kubernetesSkipCrd: true
|
|
||||||
s3:
|
|
||||||
api:
|
|
||||||
bindAddr: "[::]:3900"
|
|
||||||
region: "garage"
|
|
||||||
rootDomain: ".s3.garage.tld"
|
|
||||||
web:
|
|
||||||
bindAddr: "[::]:3902"
|
|
||||||
rootDomain: ".web.garage.tld"
|
|
||||||
index: "index.html"
|
|
||||||
admin:
|
|
||||||
apiBindAddr: "[::]:3903"
|
|
||||||
|
|
||||||
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
|
|
||||||
additionalTopLevelConfig: |-
|
|
||||||
data_fsync = true
|
|
||||||
|
|
||||||
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
|
|
||||||
# if set, ignores garage.toml
|
|
||||||
existingConfigMap: ""
|
|
||||||
|
|
||||||
# -- String Template for the garage configuration.
|
|
||||||
# if set, ignores every other garage.* value above and is rendered with `tpl`,
|
|
||||||
# so it can reference .Values/.Release/.Chart, e.g.:
|
|
||||||
# garageTomlString: |-
|
|
||||||
# metadata_dir = "/mnt/meta"
|
|
||||||
# data_dir = "/mnt/data"
|
|
||||||
# replication_factor = {{ .Values.garage.replicationFactor }}
|
|
||||||
# rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
|
|
||||||
# rpc_secret = "__RPC_SECRET_REPLACE__"
|
|
||||||
# [kubernetes_discovery]
|
|
||||||
# namespace = "{{ .Release.Namespace }}"
|
|
||||||
# service_name = "{{ include "garage.fullname" . }}"
|
|
||||||
# A rendering-verified version of this example lives in tests/configmap_test.yaml.
|
|
||||||
garageTomlString: ""
|
|
||||||
|
|
||||||
# Data persistence
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
meta:
|
|
||||||
storageClass: "fast-ssd"
|
|
||||||
size: 100Mi
|
|
||||||
# used only for daemon sets
|
|
||||||
hostPath: /var/lib/garage/meta
|
|
||||||
data:
|
|
||||||
storageClass: "standard"
|
|
||||||
size: 100Mi
|
|
||||||
# used only for daemon sets
|
|
||||||
hostPath: /var/lib/garage/data
|
|
||||||
|
|
||||||
# Deployment configuration
|
|
||||||
deployment:
|
|
||||||
# -- Switchable to DaemonSet
|
|
||||||
kind: StatefulSet
|
|
||||||
# -- Number of StatefulSet replicas/garage nodes to start
|
|
||||||
replicaCount: 3
|
|
||||||
# -- If using statefulset, allow Parallel or OrderedReady (default)
|
|
||||||
podManagementPolicy: OrderedReady
|
|
||||||
|
|
||||||
image:
|
|
||||||
# -- default to amd64 docker image
|
|
||||||
repository: dxflrs/amd64_garage
|
|
||||||
# -- set the image tag, please prefer using the chart version and not this
|
|
||||||
# to avoid compatibility issues
|
|
||||||
tag: ""
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
initImage:
|
|
||||||
repository: busybox
|
|
||||||
tag: stable
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
# -- set if you need credentials to pull your custom image. Each entry needs a
|
|
||||||
# `name:` key, matching a Secret of type kubernetes.io/dockerconfigjson.
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: my-pull-secret
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
serviceAccount:
|
|
||||||
# -- Specifies whether a service account should be created
|
|
||||||
create: true
|
|
||||||
# -- Annotations to add to the service account. Example below is for AWS IRSA.
|
|
||||||
annotations:
|
|
||||||
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/garage-s3"
|
|
||||||
# -- The name of the service account to use.
|
|
||||||
# If not set and create is true, a name is generated using the fullname template
|
|
||||||
name: ""
|
|
||||||
|
|
||||||
# -- additional pod annotations
|
|
||||||
podAnnotations:
|
|
||||||
example.com/has-an-annotation: "true"
|
|
||||||
|
|
||||||
podSecurityContext:
|
|
||||||
runAsUser: 1000
|
|
||||||
runAsGroup: 1000
|
|
||||||
fsGroup: 1000
|
|
||||||
fsGroupChangePolicy: "OnRootMismatch"
|
|
||||||
runAsNonRoot: true
|
|
||||||
|
|
||||||
securityContext:
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
|
|
||||||
service:
|
|
||||||
# -- You can rely on any service to expose your cluster
|
|
||||||
# - ClusterIP (+ Ingress)
|
|
||||||
# - NodePort (+ Ingress)
|
|
||||||
# - LoadBalancer
|
|
||||||
type: ClusterIP
|
|
||||||
# -- Annotations to add to the service. Example below is for an AWS NLB.
|
|
||||||
annotations:
|
|
||||||
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
|
|
||||||
s3:
|
|
||||||
api:
|
|
||||||
port: 3900
|
|
||||||
web:
|
|
||||||
port: 3902
|
|
||||||
# NOTE: the admin API is excluded for now as it is not consistent across nodes
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
s3:
|
|
||||||
api:
|
|
||||||
enabled: true
|
|
||||||
className: "nginx"
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
labels: {}
|
|
||||||
hosts:
|
|
||||||
# -- garage S3 API endpoint, to be used with awscli for example
|
|
||||||
- host: "s3.garage.tld"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
# -- garage S3 API endpoint, DNS style bucket access
|
|
||||||
- host: "*.s3.garage.tld"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- secretName: garage-s3-api-tls
|
|
||||||
hosts:
|
|
||||||
- s3.garage.tld
|
|
||||||
- "*.s3.garage.tld"
|
|
||||||
web:
|
|
||||||
enabled: true
|
|
||||||
className: "nginx"
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
labels: {}
|
|
||||||
hosts:
|
|
||||||
# -- wildcard website access with bucket name prefix
|
|
||||||
- host: "*.web.garage.tld"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
# -- specific bucket access with FQDN bucket
|
|
||||||
- host: "mywebpage.example.com"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- secretName: garage-s3-web-tls
|
|
||||||
hosts:
|
|
||||||
- "*.web.garage.tld"
|
|
||||||
- mywebpage.example.com
|
|
||||||
|
|
||||||
# The following are indicative for a small-size deployment, for anything serious double them.
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 2048Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 1024Mi
|
|
||||||
|
|
||||||
# -- Specifies a livenessProbe
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 3903 # or the port from garage.admin.apiBindAddr
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 30
|
|
||||||
# -- Specifies a readinessProbe
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 3903 # or the port from garage.admin.apiBindAddr
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 30
|
|
||||||
|
|
||||||
# -- Example: pin pods to a dedicated storage node pool, paired with the
|
|
||||||
# toleration below.
|
|
||||||
nodeSelector:
|
|
||||||
node-role.kubernetes.io/storage: "true"
|
|
||||||
|
|
||||||
tolerations:
|
|
||||||
- key: "dedicated"
|
|
||||||
operator: "Equal"
|
|
||||||
value: "storage"
|
|
||||||
effect: "NoSchedule"
|
|
||||||
|
|
||||||
# -- Example: spread garage replicas across different nodes, since it is a
|
|
||||||
# geo-distributed store that only helps availability if replicas don't share
|
|
||||||
# a failure domain or availability zone.
|
|
||||||
affinity:
|
|
||||||
podAntiAffinity:
|
|
||||||
preferredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
- weight: 100
|
|
||||||
podAffinityTerm:
|
|
||||||
labelSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: garage
|
|
||||||
topologyKey: kubernetes.io/hostname
|
|
||||||
|
|
||||||
# -- Optional priority class name to assign to the pods.
|
|
||||||
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
|
||||||
# This is expected to reference a PriorityClass you define yourself.
|
|
||||||
priorityClassName: "high-priority-storage"
|
|
||||||
|
|
||||||
# -- Extra container env vars. Note this is a [] of {name, value} objects (ie. a pod env stanza)
|
|
||||||
# GARAGE_ADMIN_TOKEN_FILE below points garage at the token file mounted by
|
|
||||||
# extraVolumes/extraVolumeMounts, see below.
|
|
||||||
environment:
|
|
||||||
- name: RUST_LOG
|
|
||||||
value: "garage=debug"
|
|
||||||
- name: GARAGE_ADMIN_TOKEN_FILE
|
|
||||||
value: /mnt/secrets-store/admin-token
|
|
||||||
|
|
||||||
# -- Extra volumes/volumeMounts. Both are []. Example here mounts the admin API
|
|
||||||
# token from an external secrets manager via the Secrets Store CSI driver
|
|
||||||
# (https://secrets-store-csi-driver.sigs.k8s.io/) instead of a Secret volume.
|
|
||||||
# This allows, for example, providing the tokens without creating a Kubernetes
|
|
||||||
# secret. garage reads the mounted file through GARAGE_ADMIN_TOKEN_FILE above.
|
|
||||||
extraVolumes:
|
|
||||||
- name: secrets-store
|
|
||||||
csi:
|
|
||||||
driver: secrets-store.csi.k8s.io
|
|
||||||
readOnly: true
|
|
||||||
volumeAttributes:
|
|
||||||
secretProviderClass: garage-admin-token
|
|
||||||
extraVolumeMounts:
|
|
||||||
- name: secrets-store
|
|
||||||
mountPath: /mnt/secrets-store
|
|
||||||
readOnly: true
|
|
||||||
|
|
||||||
monitoring:
|
|
||||||
metrics:
|
|
||||||
# -- If true, a service for monitoring is created with a prometheus.io/scrape annotation
|
|
||||||
enabled: true
|
|
||||||
serviceMonitor:
|
|
||||||
# -- If true, a ServiceMonitor CRD is created for a prometheus operator
|
|
||||||
# https://github.com/coreos/prometheus-operator
|
|
||||||
enabled: true
|
|
||||||
path: /metrics
|
|
||||||
# -- Defaults to the namespace the chart is deployed to; this field is
|
|
||||||
# templated, so it can also reference .Release.Namespace itself.
|
|
||||||
namespace: "monitoring"
|
|
||||||
labels:
|
|
||||||
release: prometheus
|
|
||||||
interval: 30s
|
|
||||||
scheme: http
|
|
||||||
tlsConfig: {}
|
|
||||||
scrapeTimeout: 10s
|
|
||||||
relabelings:
|
|
||||||
- sourceLabels: ["__meta_kubernetes_pod_node_name"]
|
|
||||||
targetLabel: node
|
|
||||||
tracing:
|
|
||||||
# -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317`
|
|
||||||
sink: "http://otel-collector.monitoring.svc:4317"
|
|
||||||
@@ -62,9 +62,7 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if and .Values.ingress.s3.api.enabled .Values.ingress.s3.web.enabled }}
|
|
||||||
---
|
---
|
||||||
{{ end }}
|
|
||||||
{{- if .Values.ingress.s3.web.enabled -}}
|
{{- if .Values.ingress.s3.web.enabled -}}
|
||||||
{{- $fullName := include "garage.fullname" . -}}
|
{{- $fullName := include "garage.fullname" . -}}
|
||||||
{{- $svcPort := .Values.service.s3.web.port -}}
|
{{- $svcPort := .Values.service.s3.web.port -}}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ spec:
|
|||||||
scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }}
|
scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }}
|
||||||
{{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }}
|
{{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }}
|
||||||
tlsConfig:
|
tlsConfig:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 6 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
|
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
|
||||||
relabelings:
|
relabelings:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 6 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
jobLabel: "{{ .Release.Name }}"
|
jobLabel: "{{ .Release.Name }}"
|
||||||
selector:
|
selector:
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
suite: rbac
|
|
||||||
templates:
|
|
||||||
- templates/clusterrole.yaml
|
|
||||||
tests:
|
|
||||||
- it: allows managing the garage CRD by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 2
|
|
||||||
- documentIndex: 0
|
|
||||||
isKind:
|
|
||||||
of: ClusterRole
|
|
||||||
- documentIndex: 0
|
|
||||||
contains:
|
|
||||||
path: rules[0].resources
|
|
||||||
content: customresourcedefinitions
|
|
||||||
- documentIndex: 1
|
|
||||||
isKind:
|
|
||||||
of: ClusterRoleBinding
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: subjects[0].name
|
|
||||||
value: RELEASE-NAME-garage
|
|
||||||
|
|
||||||
- it: skips the CRD management rule when the CRD is installed manually
|
|
||||||
set:
|
|
||||||
garage.kubernetesSkipCrd: true
|
|
||||||
asserts:
|
|
||||||
- documentIndex: 0
|
|
||||||
notContains:
|
|
||||||
path: rules[0].resources
|
|
||||||
content: customresourcedefinitions
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Integration-style suite: renders the whole chart with complex-values.yaml
|
|
||||||
# (many non-default features combined at once) and checks that they don't
|
|
||||||
# clobber each other, rather than testing any single feature in isolation
|
|
||||||
# (that's what the other tests/*_test.yaml suites are for).
|
|
||||||
suite: complex-values integration
|
|
||||||
templates:
|
|
||||||
- templates/workload.yaml
|
|
||||||
- templates/service.yaml
|
|
||||||
- templates/service-headless.yaml
|
|
||||||
- templates/ingress.yaml
|
|
||||||
- templates/servicemonitor.yaml
|
|
||||||
- templates/serviceaccount.yaml
|
|
||||||
- templates/configmap.yaml
|
|
||||||
- templates/clusterrole.yaml
|
|
||||||
tests:
|
|
||||||
- it: renders a self-consistent deployment with every optional feature enabled
|
|
||||||
values:
|
|
||||||
- ../complex-values.yaml
|
|
||||||
asserts:
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
isKind:
|
|
||||||
of: StatefulSet
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
equal:
|
|
||||||
path: metadata.labels.team
|
|
||||||
value: platform-infrastructure
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
equal:
|
|
||||||
path: spec.template.spec.containers[0].env[0].name
|
|
||||||
value: RUST_LOG
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: secrets-store
|
|
||||||
csi:
|
|
||||||
driver: secrets-store.csi.k8s.io
|
|
||||||
readOnly: true
|
|
||||||
volumeAttributes:
|
|
||||||
secretProviderClass: garage-admin-token
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
equal:
|
|
||||||
path: spec.template.spec.containers[0].env[1].name
|
|
||||||
value: GARAGE_ADMIN_TOKEN_FILE
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
equal:
|
|
||||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
|
||||||
value: fast-ssd
|
|
||||||
- template: templates/workload.yaml
|
|
||||||
contains:
|
|
||||||
path: spec.template.spec.imagePullSecrets
|
|
||||||
content:
|
|
||||||
name: my-pull-secret
|
|
||||||
- template: templates/service.yaml
|
|
||||||
hasDocuments:
|
|
||||||
count: 2 # main service + metrics service, since monitoring.metrics.enabled is true here
|
|
||||||
- template: templates/service-headless.yaml
|
|
||||||
hasDocuments:
|
|
||||||
count: 1 # StatefulSet still gets a headless service
|
|
||||||
- template: templates/ingress.yaml
|
|
||||||
hasDocuments:
|
|
||||||
count: 2 # both s3 api and s3 web ingresses enabled together
|
|
||||||
- template: templates/servicemonitor.yaml
|
|
||||||
hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- template: templates/servicemonitor.yaml
|
|
||||||
equal:
|
|
||||||
path: spec.endpoints[0].relabelings[0].targetLabel
|
|
||||||
value: node
|
|
||||||
- template: templates/serviceaccount.yaml
|
|
||||||
equal:
|
|
||||||
path: metadata.annotations["eks.amazonaws.com/role-arn"]
|
|
||||||
value: "arn:aws:iam::123456789012:role/garage-s3"
|
|
||||||
- template: templates/configmap.yaml
|
|
||||||
matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'data_fsync = true'
|
|
||||||
- template: templates/clusterrole.yaml
|
|
||||||
documentIndex: 0
|
|
||||||
notContains:
|
|
||||||
path: rules[0].resources
|
|
||||||
content: customresourcedefinitions # garage.kubernetesSkipCrd is true here
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
suite: configmap
|
|
||||||
templates:
|
|
||||||
- templates/configmap.yaml
|
|
||||||
tests:
|
|
||||||
- it: renders garage.toml with the default configuration
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: ConfigMap
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-config
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'metadata_dir = "/mnt/meta"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'data_dir = "/mnt/data"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'db_engine = "lmdb"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'block_size = "1048576"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'replication_factor = 3'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'consistency_mode = "consistent"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'compression_level = 1'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'rpc_bind_addr = "\[::\]:3901"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'rpc_secret = "__RPC_SECRET_REPLACE__"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: '(?s)\[kubernetes_discovery\]\s*namespace = "NAMESPACE"\s*service_name = "RELEASE-NAME-garage"\s*skip_crd = false'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: '(?s)\[s3_api\]\s*s3_region = "garage"\s*api_bind_addr = "\[::\]:3900"\s*root_domain = "\.s3\.garage\.tld"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: '(?s)\[s3_web\]\s*bind_addr = "\[::\]:3902"\s*root_domain = "\.web\.garage\.tld"\s*index = "index.html"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: '(?s)\[admin\]\s*api_bind_addr = "\[::\]:3903"'
|
|
||||||
- notMatchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'metadata_auto_snapshot_interval'
|
|
||||||
- notMatchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'trace_sink'
|
|
||||||
|
|
||||||
- it: reflects custom garage settings, bootstrap peers and additional config
|
|
||||||
set:
|
|
||||||
garage.dbEngine: sqlite
|
|
||||||
garage.blockSize: "2097152"
|
|
||||||
garage.replicationFactor: "5"
|
|
||||||
garage.consistencyMode: degraded
|
|
||||||
garage.compressionLevel: "3"
|
|
||||||
garage.metadataAutoSnapshotInterval: 6h
|
|
||||||
garage.bootstrapPeers:
|
|
||||||
- abc@peer1:3901
|
|
||||||
- def@peer2:3901
|
|
||||||
garage.additionalTopLevelConfig: "data_fsync = true"
|
|
||||||
monitoring.tracing.sink: http://otel:4317
|
|
||||||
asserts:
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'db_engine = "sqlite"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'block_size = "2097152"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'replication_factor = 5'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'consistency_mode = "degraded"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'compression_level = 3'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'metadata_auto_snapshot_interval = "6h"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'bootstrap_peers = \["abc@peer1:3901"\s*, "def@peer2:3901"'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'data_fsync = true'
|
|
||||||
- matchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'trace_sink = "http://otel:4317"'
|
|
||||||
|
|
||||||
- it: uses garageTomlString verbatim when set, ignoring the structured values
|
|
||||||
set:
|
|
||||||
garage.garageTomlString: |-
|
|
||||||
metadata_dir = "/custom/meta"
|
|
||||||
replication_factor = 1
|
|
||||||
garage.dbEngine: sqlite
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
value: |-
|
|
||||||
metadata_dir = "/custom/meta"
|
|
||||||
replication_factor = 1
|
|
||||||
- notMatchRegex:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
pattern: 'db_engine'
|
|
||||||
|
|
||||||
- it: templates garageTomlString against the release and values context
|
|
||||||
set:
|
|
||||||
garage.garageTomlString: |-
|
|
||||||
# namespace: {{ .Release.Namespace }}
|
|
||||||
replication_factor = {{ .Values.garage.replicationFactor }}
|
|
||||||
garage.replicationFactor: "7"
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: data["garage.toml"]
|
|
||||||
value: |-
|
|
||||||
# namespace: NAMESPACE
|
|
||||||
replication_factor = 7
|
|
||||||
|
|
||||||
- it: does not render a ConfigMap when an existing one is referenced
|
|
||||||
set:
|
|
||||||
garage.existingConfigMap: my-external-cm
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
suite: ingress
|
|
||||||
templates:
|
|
||||||
- templates/ingress.yaml
|
|
||||||
tests:
|
|
||||||
- it: renders no ingress by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
|
|
||||||
- it: renders api and web ingresses with tls when enabled
|
|
||||||
values:
|
|
||||||
- ./values/ingress.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 2
|
|
||||||
- isKind:
|
|
||||||
of: Ingress
|
|
||||||
- documentIndex: 0
|
|
||||||
equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-s3-api
|
|
||||||
- documentIndex: 0
|
|
||||||
equal:
|
|
||||||
path: spec.ingressClassName
|
|
||||||
value: nginx
|
|
||||||
- documentIndex: 0
|
|
||||||
equal:
|
|
||||||
path: spec.rules[0].host
|
|
||||||
value: s3.example.com
|
|
||||||
- documentIndex: 0
|
|
||||||
equal:
|
|
||||||
path: spec.tls[0].secretName
|
|
||||||
value: garage-s3-api-tls
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-s3-web
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: spec.rules[0].host
|
|
||||||
value: "*.web.example.com"
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: spec.tls[0].secretName
|
|
||||||
value: garage-s3-web-tls
|
|
||||||
|
|
||||||
- it: can enable only the s3 api ingress
|
|
||||||
set:
|
|
||||||
ingress.s3.api.enabled: true
|
|
||||||
ingress.s3.api.hosts[0].host: s3.example.com
|
|
||||||
ingress.s3.api.hosts[0].paths[0].path: /
|
|
||||||
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-s3-api
|
|
||||||
|
|
||||||
- it: omits ingressClassName and tls when neither is configured
|
|
||||||
set:
|
|
||||||
ingress.s3.api.enabled: true
|
|
||||||
ingress.s3.api.hosts[0].host: s3.example.com
|
|
||||||
ingress.s3.api.hosts[0].paths[0].path: /
|
|
||||||
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
|
|
||||||
asserts:
|
|
||||||
- isNull:
|
|
||||||
path: spec.ingressClassName
|
|
||||||
- isNull:
|
|
||||||
path: spec.tls
|
|
||||||
|
|
||||||
- it: renders multiple hosts on the same ingress
|
|
||||||
set:
|
|
||||||
ingress.s3.api.enabled: true
|
|
||||||
ingress.s3.api.hosts:
|
|
||||||
- host: s3.example.com
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
- host: s3-alt.example.com
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- lengthEqual:
|
|
||||||
path: spec.rules
|
|
||||||
count: 2
|
|
||||||
- equal:
|
|
||||||
path: spec.rules[0].host
|
|
||||||
value: s3.example.com
|
|
||||||
- equal:
|
|
||||||
path: spec.rules[1].host
|
|
||||||
value: s3-alt.example.com
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
suite: naming and common labels
|
|
||||||
templates:
|
|
||||||
- templates/workload.yaml
|
|
||||||
- templates/configmap.yaml
|
|
||||||
tests:
|
|
||||||
- it: applies commonLabels alongside the default chart labels
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
commonLabels:
|
|
||||||
team: storage
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.labels.team
|
|
||||||
value: storage
|
|
||||||
- equal:
|
|
||||||
path: metadata.labels["app.kubernetes.io/managed-by"]
|
|
||||||
value: Helm
|
|
||||||
|
|
||||||
- it: uses fullnameOverride verbatim for resource names
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
fullnameOverride: my-garage-cluster
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: my-garage-cluster
|
|
||||||
- equal:
|
|
||||||
path: spec.serviceName
|
|
||||||
value: my-garage-cluster-headless
|
|
||||||
|
|
||||||
- it: does not double-prefix when the release name already contains the chart name
|
|
||||||
template: templates/workload.yaml
|
|
||||||
release:
|
|
||||||
name: garage
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: garage
|
|
||||||
|
|
||||||
- it: prefixes the release name with the chart name otherwise
|
|
||||||
template: templates/workload.yaml
|
|
||||||
release:
|
|
||||||
name: prod
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: prod-garage
|
|
||||||
|
|
||||||
- it: truncates an overly long fullname to 63 characters and trims a trailing dash
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
fullnameOverride: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-suffix-that-will-be-cut-off
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
suite: rpc secret
|
|
||||||
templates:
|
|
||||||
- templates/secret.yaml
|
|
||||||
tests:
|
|
||||||
- it: generates a Secret holding the rpc secret by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: Secret
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-rpc-secret
|
|
||||||
- equal:
|
|
||||||
path: type
|
|
||||||
value: Opaque
|
|
||||||
- isNotNull:
|
|
||||||
path: data.rpcSecret
|
|
||||||
|
|
||||||
- it: does not render a Secret when an existing one is referenced
|
|
||||||
values:
|
|
||||||
- ./values/existing-secret.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
|
|
||||||
- it: base64-encodes an explicitly provided rpc secret
|
|
||||||
set:
|
|
||||||
garage.rpcSecret: my-plain-secret
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: data.rpcSecret
|
|
||||||
value: bXktcGxhaW4tc2VjcmV0
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
suite: headless service
|
|
||||||
templates:
|
|
||||||
- templates/service-headless.yaml
|
|
||||||
tests:
|
|
||||||
- it: creates a headless service for a StatefulSet by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: Service
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-headless
|
|
||||||
- equal:
|
|
||||||
path: spec.clusterIP
|
|
||||||
value: None
|
|
||||||
- equal:
|
|
||||||
path: spec.type
|
|
||||||
value: ClusterIP
|
|
||||||
|
|
||||||
- it: does not create a headless service for a DaemonSet
|
|
||||||
values:
|
|
||||||
- ./values/daemonset.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
suite: service
|
|
||||||
templates:
|
|
||||||
- templates/service.yaml
|
|
||||||
tests:
|
|
||||||
- it: creates a ClusterIP service with s3-api and s3-web ports by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: Service
|
|
||||||
- equal:
|
|
||||||
path: spec.type
|
|
||||||
value: ClusterIP
|
|
||||||
- equal:
|
|
||||||
path: spec.ports[0].name
|
|
||||||
value: s3-api
|
|
||||||
- equal:
|
|
||||||
path: spec.ports[0].port
|
|
||||||
value: 3900
|
|
||||||
- equal:
|
|
||||||
path: spec.ports[1].name
|
|
||||||
value: s3-web
|
|
||||||
- equal:
|
|
||||||
path: spec.ports[1].port
|
|
||||||
value: 3902
|
|
||||||
|
|
||||||
- it: honors a custom service type and port
|
|
||||||
set:
|
|
||||||
service.type: LoadBalancer
|
|
||||||
service.s3.api.port: 9000
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.type
|
|
||||||
value: LoadBalancer
|
|
||||||
- equal:
|
|
||||||
path: spec.ports[0].port
|
|
||||||
value: 9000
|
|
||||||
|
|
||||||
- it: does not create a metrics service by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
|
|
||||||
- it: adds a headless metrics service when monitoring is enabled
|
|
||||||
values:
|
|
||||||
- ./values/monitoring.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 2
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage-metrics
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: spec.clusterIP
|
|
||||||
value: None
|
|
||||||
- documentIndex: 1
|
|
||||||
equal:
|
|
||||||
path: metadata.annotations["prometheus.io/scrape"]
|
|
||||||
value: "true"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
suite: service account
|
|
||||||
templates:
|
|
||||||
- templates/serviceaccount.yaml
|
|
||||||
tests:
|
|
||||||
- it: creates a ServiceAccount by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: ServiceAccount
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage
|
|
||||||
|
|
||||||
- it: does not create a ServiceAccount when disabled
|
|
||||||
values:
|
|
||||||
- ./values/minimal.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
|
|
||||||
- it: honors a custom service account name
|
|
||||||
set:
|
|
||||||
serviceAccount.name: my-garage-sa
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: my-garage-sa
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
suite: service monitor
|
|
||||||
templates:
|
|
||||||
- templates/servicemonitor.yaml
|
|
||||||
tests:
|
|
||||||
- it: renders no ServiceMonitor by default
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
|
|
||||||
- it: renders no ServiceMonitor when only metrics are enabled
|
|
||||||
set:
|
|
||||||
monitoring.metrics.enabled: true
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 0
|
|
||||||
|
|
||||||
- it: renders a ServiceMonitor when explicitly enabled
|
|
||||||
values:
|
|
||||||
- ./values/monitoring.yaml
|
|
||||||
asserts:
|
|
||||||
- hasDocuments:
|
|
||||||
count: 1
|
|
||||||
- isKind:
|
|
||||||
of: ServiceMonitor
|
|
||||||
- equal:
|
|
||||||
path: metadata.name
|
|
||||||
value: RELEASE-NAME-garage
|
|
||||||
- equal:
|
|
||||||
path: metadata.namespace
|
|
||||||
value: NAMESPACE
|
|
||||||
- equal:
|
|
||||||
path: spec.endpoints[0].interval
|
|
||||||
value: 30s
|
|
||||||
|
|
||||||
- it: templates a custom namespace against the release context
|
|
||||||
values:
|
|
||||||
- ./values/monitoring.yaml
|
|
||||||
set:
|
|
||||||
monitoring.metrics.serviceMonitor.namespace: "{{ .Release.Namespace }}-monitoring"
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.namespace
|
|
||||||
value: NAMESPACE-monitoring
|
|
||||||
|
|
||||||
- it: applies custom labels, tlsConfig and relabelings
|
|
||||||
values:
|
|
||||||
- ./values/monitoring.yaml
|
|
||||||
set:
|
|
||||||
monitoring.metrics.serviceMonitor.labels:
|
|
||||||
team: storage
|
|
||||||
monitoring.metrics.serviceMonitor.tlsConfig:
|
|
||||||
insecureSkipVerify: true
|
|
||||||
monitoring.metrics.serviceMonitor.relabelings:
|
|
||||||
- sourceLabels: ["__meta_kubernetes_pod_name"]
|
|
||||||
targetLabel: pod
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: metadata.labels.team
|
|
||||||
value: storage
|
|
||||||
- equal:
|
|
||||||
path: spec.endpoints[0].tlsConfig.insecureSkipVerify
|
|
||||||
value: true
|
|
||||||
- equal:
|
|
||||||
path: spec.endpoints[0].relabelings[0].targetLabel
|
|
||||||
value: pod
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Run garage as a DaemonSet (one pod per node) instead of the default StatefulSet,
|
|
||||||
# using hostPath volumes for meta/data persistence.
|
|
||||||
deployment:
|
|
||||||
kind: DaemonSet
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
meta:
|
|
||||||
hostPath: /var/lib/garage/meta
|
|
||||||
data:
|
|
||||||
hostPath: /var/lib/garage/data
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Use a pre-existing Kubernetes Secret for the RPC secret instead of letting
|
|
||||||
# the chart generate/manage one.
|
|
||||||
garage:
|
|
||||||
rpcSecret: ""
|
|
||||||
existingRpcSecret: "garage-rpc-secret-external"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Expose both the S3 API and website endpoints through Ingress, with TLS.
|
|
||||||
ingress:
|
|
||||||
s3:
|
|
||||||
api:
|
|
||||||
enabled: true
|
|
||||||
className: "nginx"
|
|
||||||
hosts:
|
|
||||||
- host: "s3.example.com"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- secretName: garage-s3-api-tls
|
|
||||||
hosts:
|
|
||||||
- s3.example.com
|
|
||||||
web:
|
|
||||||
enabled: true
|
|
||||||
className: "nginx"
|
|
||||||
hosts:
|
|
||||||
- host: "*.web.example.com"
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- secretName: garage-s3-web-tls
|
|
||||||
hosts:
|
|
||||||
- "*.web.example.com"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Minimal single-node deployment without persistent storage or a dedicated
|
|
||||||
# service account, e.g. for local testing.
|
|
||||||
deployment:
|
|
||||||
replicaCount: 1
|
|
||||||
persistence:
|
|
||||||
enabled: false
|
|
||||||
serviceAccount:
|
|
||||||
create: false
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Enable Prometheus metrics scraping and a ServiceMonitor for the prometheus-operator.
|
|
||||||
monitoring:
|
|
||||||
metrics:
|
|
||||||
enabled: true
|
|
||||||
serviceMonitor:
|
|
||||||
enabled: true
|
|
||||||
interval: 30s
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
suite: workload (StatefulSet/DaemonSet)
|
|
||||||
templates:
|
|
||||||
- templates/workload.yaml
|
|
||||||
- templates/configmap.yaml
|
|
||||||
tests:
|
|
||||||
- it: defaults to a StatefulSet with 3 replicas and 2 volumes
|
|
||||||
template: templates/workload.yaml
|
|
||||||
asserts:
|
|
||||||
- isKind:
|
|
||||||
of: StatefulSet
|
|
||||||
- equal:
|
|
||||||
path: spec.replicas
|
|
||||||
value: 3
|
|
||||||
- equal:
|
|
||||||
path: spec.podManagementPolicy
|
|
||||||
value: OrderedReady
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.volumes[1].name
|
|
||||||
value: etc
|
|
||||||
- lengthEqual:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
count: 2
|
|
||||||
- isNotNull:
|
|
||||||
path: spec.volumeClaimTemplates
|
|
||||||
|
|
||||||
- it: uses a StatefulSet with PVC-backed volumeClaimTemplates by default
|
|
||||||
template: templates/workload.yaml
|
|
||||||
asserts:
|
|
||||||
- isKind:
|
|
||||||
of: StatefulSet
|
|
||||||
- isNotNull:
|
|
||||||
path: spec.volumeClaimTemplates
|
|
||||||
- equal:
|
|
||||||
path: spec.volumeClaimTemplates[0].spec.resources.requests.storage
|
|
||||||
value: 100Mi
|
|
||||||
|
|
||||||
- it: switches to a DaemonSet with hostPath volumes when requested
|
|
||||||
template: templates/workload.yaml
|
|
||||||
values:
|
|
||||||
- ./values/daemonset.yaml
|
|
||||||
asserts:
|
|
||||||
- isKind:
|
|
||||||
of: DaemonSet
|
|
||||||
- isNull:
|
|
||||||
path: spec.replicas
|
|
||||||
- isNull:
|
|
||||||
path: spec.volumeClaimTemplates
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: meta
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/garage/meta
|
|
||||||
type: DirectoryOrCreate
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: data
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/garage/data
|
|
||||||
type: DirectoryOrCreate
|
|
||||||
|
|
||||||
- it: renders emptyDir volumes when persistence is disabled
|
|
||||||
template: templates/workload.yaml
|
|
||||||
values:
|
|
||||||
- ./values/minimal.yaml
|
|
||||||
asserts:
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: meta
|
|
||||||
emptyDir: {}
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: data
|
|
||||||
emptyDir: {}
|
|
||||||
- isNull:
|
|
||||||
path: spec.volumeClaimTemplates
|
|
||||||
|
|
||||||
- it: honors a custom replicaCount
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
deployment.replicaCount: 5
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.replicas
|
|
||||||
value: 5
|
|
||||||
|
|
||||||
- it: points the init container at the configured rpc secret
|
|
||||||
template: templates/workload.yaml
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
|
|
||||||
value: RELEASE-NAME-garage-rpc-secret
|
|
||||||
|
|
||||||
- it: points the init container at an existing rpc secret when configured
|
|
||||||
template: templates/workload.yaml
|
|
||||||
values:
|
|
||||||
- ./values/existing-secret.yaml
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
|
|
||||||
value: garage-rpc-secret-external
|
|
||||||
|
|
||||||
- it: sets the container image from repository and tag
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
image.repository: dxflrs/amd64_garage
|
|
||||||
image.tag: v1.2.3
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.containers[0].image
|
|
||||||
value: dxflrs/amd64_garage:v1.2.3
|
|
||||||
|
|
||||||
- it: falls back to the chart appVersion when no image tag is set
|
|
||||||
template: templates/workload.yaml
|
|
||||||
asserts:
|
|
||||||
- matchRegex:
|
|
||||||
path: spec.template.spec.containers[0].image
|
|
||||||
pattern: ^dxflrs/amd64_garage:v
|
|
||||||
|
|
||||||
- it: omits storageClassName from volumeClaimTemplates by default
|
|
||||||
template: templates/workload.yaml
|
|
||||||
asserts:
|
|
||||||
- isNull:
|
|
||||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
|
||||||
- isNull:
|
|
||||||
path: spec.volumeClaimTemplates[1].spec.storageClassName
|
|
||||||
|
|
||||||
- it: sets storageClassName in volumeClaimTemplates when configured
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
persistence.meta.storageClass: fast-storage
|
|
||||||
persistence.data.storageClass: slow-storage
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
|
||||||
value: fast-storage
|
|
||||||
- equal:
|
|
||||||
path: spec.volumeClaimTemplates[1].spec.storageClassName
|
|
||||||
value: slow-storage
|
|
||||||
|
|
||||||
- it: renders emptyDir volumes for a DaemonSet when persistence is disabled
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
deployment.kind: DaemonSet
|
|
||||||
persistence.enabled: false
|
|
||||||
asserts:
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: meta
|
|
||||||
emptyDir: {}
|
|
||||||
- contains:
|
|
||||||
path: spec.template.spec.volumes
|
|
||||||
content:
|
|
||||||
name: data
|
|
||||||
emptyDir: {}
|
|
||||||
|
|
||||||
- it: mounts the existing ConfigMap volume when configured
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
garage.existingConfigMap: my-external-cm
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.volumes[0].configMap.name
|
|
||||||
value: my-external-cm
|
|
||||||
|
|
||||||
- it: uses a custom service account name without creating one when disabled
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
serviceAccount.create: false
|
|
||||||
serviceAccount.name: my-external-sa
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.serviceAccountName
|
|
||||||
value: my-external-sa
|
|
||||||
|
|
||||||
- it: falls back to the default service account when disabled without a custom name
|
|
||||||
template: templates/workload.yaml
|
|
||||||
set:
|
|
||||||
serviceAccount.create: false
|
|
||||||
asserts:
|
|
||||||
- equal:
|
|
||||||
path: spec.template.spec.serviceAccountName
|
|
||||||
value: default
|
|
||||||
@@ -246,14 +246,10 @@ affinity: {}
|
|||||||
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
||||||
priorityClassName: ""
|
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:
|
||||||
|
|||||||
+36
-6
@@ -688,11 +688,26 @@ pub struct ClusterLayoutSkipDeadNodesResponse {
|
|||||||
|
|
||||||
// ---- ListKeys ----
|
// ---- ListKeys ----
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, IntoParams)]
|
||||||
pub struct ListKeysRequest;
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct ListKeysRequest {
|
||||||
|
/// Returned detailed informations in the same format as GetKeyInfo for each bucket
|
||||||
|
#[serde(default)]
|
||||||
|
pub details: bool,
|
||||||
|
/// Key ID of the first key to return
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: Option<String>,
|
||||||
|
/// Maximum number of keys to return in a single call
|
||||||
|
#[serde(default)]
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ListKeysResponse(pub Vec<ListKeysResponseItem>);
|
#[serde(untagged)]
|
||||||
|
pub enum ListKeysResponse {
|
||||||
|
WithoutDetails(Vec<ListKeysResponseItem>),
|
||||||
|
WithDetails(Vec<GetKeyInfoResponse>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -830,11 +845,26 @@ pub struct DeleteKeyResponse;
|
|||||||
|
|
||||||
// ---- ListBuckets ----
|
// ---- ListBuckets ----
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, IntoParams)]
|
||||||
pub struct ListBucketsRequest;
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct ListBucketsRequest {
|
||||||
|
/// Returned detailed informations in the same format as GetBucketInfo for each bucket
|
||||||
|
#[serde(default)]
|
||||||
|
pub details: bool,
|
||||||
|
/// Bucket ID of the first bucket to return
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: Option<String>,
|
||||||
|
/// Maximum number of buckets to return in a single call
|
||||||
|
#[serde(default)]
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ListBucketsResponse(pub Vec<ListBucketsResponseItem>);
|
#[serde(untagged)]
|
||||||
|
pub enum ListBucketsResponse {
|
||||||
|
WithoutDetails(Vec<ListBucketsResponseItem>),
|
||||||
|
WithDetails(Vec<GetBucketInfoResponse>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
+55
-31
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
@@ -32,47 +33,70 @@ impl RequestHandler for ListBucketsRequest {
|
|||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
_admin: &Admin,
|
_admin: &Admin,
|
||||||
) -> Result<ListBucketsResponse, Error> {
|
) -> Result<ListBucketsResponse, Error> {
|
||||||
|
let limit = self
|
||||||
|
.limit
|
||||||
|
.unwrap_or_else(|| if self.details { 1000 } else { 10_000 });
|
||||||
|
|
||||||
|
let offset = match self.offset {
|
||||||
|
Some(id) => Some(parse_bucket_id(&id)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let buckets = garage
|
let buckets = garage
|
||||||
.bucket_table
|
.bucket_table
|
||||||
.get_range(
|
.get_range(
|
||||||
&EmptyKey,
|
&EmptyKey,
|
||||||
None,
|
offset,
|
||||||
Some(DeletedFilter::NotDeleted),
|
Some(DeletedFilter::NotDeleted),
|
||||||
1_000_000,
|
limit,
|
||||||
EnumerationOrder::Forward,
|
EnumerationOrder::Forward,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let res = buckets
|
if self.details {
|
||||||
.into_iter()
|
let mut stream = buckets
|
||||||
.map(|b| {
|
.into_iter()
|
||||||
let state = b.state.as_option().unwrap();
|
.map(|b| bucket_info_results(garage, b.id))
|
||||||
ListBucketsResponseItem {
|
.collect::<futures::stream::FuturesOrdered<_>>();
|
||||||
id: hex::encode(b.id),
|
|
||||||
created: DateTime::from_timestamp_millis(state.creation_date as i64)
|
|
||||||
.expect("invalid timestamp stored in db"),
|
|
||||||
global_aliases: state
|
|
||||||
.aliases
|
|
||||||
.items()
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, _, a)| *a)
|
|
||||||
.map(|(n, _, _)| n.to_string())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
local_aliases: state
|
|
||||||
.local_aliases
|
|
||||||
.items()
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, _, a)| *a)
|
|
||||||
.map(|((k, n), _, _)| BucketLocalAlias {
|
|
||||||
access_key_id: k.to_string(),
|
|
||||||
alias: n.to_string(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(ListBucketsResponse(res))
|
let mut res = vec![];
|
||||||
|
while let Some(next) = stream.next().await {
|
||||||
|
res.push(next?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ListBucketsResponse::WithDetails(res))
|
||||||
|
} else {
|
||||||
|
let res = buckets
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| {
|
||||||
|
let state = b.state.as_option().unwrap();
|
||||||
|
ListBucketsResponseItem {
|
||||||
|
id: hex::encode(b.id),
|
||||||
|
created: DateTime::from_timestamp_millis(state.creation_date as i64)
|
||||||
|
.expect("invalid timestamp stored in db"),
|
||||||
|
global_aliases: state
|
||||||
|
.aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|(n, _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
local_aliases: state
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|((k, n), _, _)| BucketLocalAlias {
|
||||||
|
access_key_id: k.to_string(),
|
||||||
|
alias: n.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(ListBucketsResponse::WithoutDetails(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-23
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
use garage_util::time::now_msec;
|
use garage_util::time::now_msec;
|
||||||
@@ -20,37 +21,57 @@ impl RequestHandler for ListKeysRequest {
|
|||||||
async fn handle(self, garage: &Arc<Garage>, _admin: &Admin) -> Result<ListKeysResponse, Error> {
|
async fn handle(self, garage: &Arc<Garage>, _admin: &Admin) -> Result<ListKeysResponse, Error> {
|
||||||
let now = now_msec();
|
let now = now_msec();
|
||||||
|
|
||||||
let res = garage
|
let limit = self
|
||||||
|
.limit
|
||||||
|
.unwrap_or_else(|| if self.details { 1000 } else { 10_000 });
|
||||||
|
|
||||||
|
let keys = garage
|
||||||
.key_table
|
.key_table
|
||||||
.get_range(
|
.get_range(
|
||||||
&EmptyKey,
|
&EmptyKey,
|
||||||
None,
|
self.offset,
|
||||||
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||||
10000,
|
limit,
|
||||||
EnumerationOrder::Forward,
|
EnumerationOrder::Forward,
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.iter()
|
|
||||||
.map(|k| {
|
|
||||||
let p = k.params().unwrap();
|
|
||||||
|
|
||||||
ListKeysResponseItem {
|
if self.details {
|
||||||
id: k.key_id.to_string(),
|
let mut stream = keys
|
||||||
name: p.name.get().clone(),
|
.into_iter()
|
||||||
created: p.created.map(|x| {
|
.map(|k| key_info_results(garage, k, false))
|
||||||
DateTime::from_timestamp_millis(x as i64)
|
.collect::<futures::stream::FuturesOrdered<_>>();
|
||||||
.expect("invalid timestamp stored in db")
|
|
||||||
}),
|
|
||||||
expiration: p.expiration.get().inner().map(|x| {
|
|
||||||
DateTime::from_timestamp_millis(x.0 as i64)
|
|
||||||
.expect("invalid timestamp stored in db")
|
|
||||||
}),
|
|
||||||
expired: p.is_expired(now),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(ListKeysResponse(res))
|
let mut res = vec![];
|
||||||
|
while let Some(next) = stream.next().await {
|
||||||
|
res.push(next?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ListKeysResponse::WithDetails(res))
|
||||||
|
} else {
|
||||||
|
let res = keys
|
||||||
|
.iter()
|
||||||
|
.map(|k| {
|
||||||
|
let p = k.params().unwrap();
|
||||||
|
|
||||||
|
ListKeysResponseItem {
|
||||||
|
id: k.key_id.to_string(),
|
||||||
|
name: p.name.get().clone(),
|
||||||
|
created: p.created.map(|x| {
|
||||||
|
DateTime::from_timestamp_millis(x as i64)
|
||||||
|
.expect("invalid timestamp stored in db")
|
||||||
|
}),
|
||||||
|
expiration: p.expiration.get().inner().map(|x| {
|
||||||
|
DateTime::from_timestamp_millis(x.0 as i64)
|
||||||
|
.expect("invalid timestamp stored in db")
|
||||||
|
}),
|
||||||
|
expired: p.is_expired(now),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(ListKeysResponse::WithoutDetails(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ fn ClusterLayoutSkipDeadNodes() {}
|
|||||||
path = "/v2/ListKeys",
|
path = "/v2/ListKeys",
|
||||||
tag = "Access key",
|
tag = "Access key",
|
||||||
description = "Returns all API access keys in the cluster.",
|
description = "Returns all API access keys in the cluster.",
|
||||||
|
params(ListKeysRequest),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)", body = ListKeysResponse),
|
(status = 200, description = "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)", body = ListKeysResponse),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "Internal server error")
|
||||||
@@ -453,6 +454,7 @@ fn DeleteKey() {}
|
|||||||
path = "/v2/ListBuckets",
|
path = "/v2/ListBuckets",
|
||||||
tag = "Bucket",
|
tag = "Bucket",
|
||||||
description = "List all the buckets on the cluster with their UUID and their global and local aliases.",
|
description = "List all the buckets on the cluster with their UUID and their global and local aliases.",
|
||||||
|
params(ListBucketsRequest),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Returns the UUID of all the buckets and all their aliases", body = ListBucketsResponse),
|
(status = 200, description = "Returns the UUID of all the buckets and all their aliases", body = ListBucketsResponse),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "Internal server error")
|
||||||
@@ -876,7 +878,7 @@ impl Modify for SecurityAddon {
|
|||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
info(
|
info(
|
||||||
version = "v2.3.0",
|
version = "v2.4.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.
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ impl AdminApiRequest {
|
|||||||
POST CreateKey (body),
|
POST CreateKey (body),
|
||||||
POST ImportKey (body),
|
POST ImportKey (body),
|
||||||
POST DeleteKey (query::id),
|
POST DeleteKey (query::id),
|
||||||
GET ListKeys (),
|
GET ListKeys (parse_default(false)::details, query_opt::offset, opt_parse::limit),
|
||||||
// Bucket endpoints
|
// Bucket endpoints
|
||||||
GET GetBucketInfo (query_opt::id, query_opt::global_alias, query_opt::search),
|
GET GetBucketInfo (query_opt::id, query_opt::global_alias, query_opt::search),
|
||||||
GET ListBuckets (),
|
GET ListBuckets (parse_default(false)::details, query_opt::offset, opt_parse::limit),
|
||||||
POST CreateBucket (body),
|
POST CreateBucket (body),
|
||||||
POST DeleteBucket (query::id),
|
POST DeleteBucket (query::id),
|
||||||
POST UpdateBucket (body_field, query::id),
|
POST UpdateBucket (body_field, query::id),
|
||||||
@@ -129,7 +129,7 @@ impl AdminApiRequest {
|
|||||||
)),
|
)),
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
Endpoint::ListKeys => Ok(AdminApiRequest::ListKeys(ListKeysRequest)),
|
Endpoint::ListKeys => Ok(AdminApiRequest::ListKeys(ListKeysRequest::default())),
|
||||||
Endpoint::GetKeyInfo {
|
Endpoint::GetKeyInfo {
|
||||||
id,
|
id,
|
||||||
search,
|
search,
|
||||||
@@ -161,7 +161,9 @@ impl AdminApiRequest {
|
|||||||
// Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })),
|
// Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })),
|
||||||
|
|
||||||
// Buckets
|
// Buckets
|
||||||
Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)),
|
Endpoint::ListBuckets => {
|
||||||
|
Ok(AdminApiRequest::ListBuckets(ListBucketsRequest::default()))
|
||||||
|
}
|
||||||
Endpoint::GetBucketInfo { id, global_alias } => {
|
Endpoint::GetBucketInfo { id, global_alias } => {
|
||||||
Ok(AdminApiRequest::GetBucketInfo(GetBucketInfoRequest {
|
Ok(AdminApiRequest::GetBucketInfo(GetBucketInfoRequest {
|
||||||
id,
|
id,
|
||||||
@@ -271,6 +273,9 @@ generateQueryParameters! {
|
|||||||
"accessKeyId" => access_key_id,
|
"accessKeyId" => access_key_id,
|
||||||
"showSecretKey" => show_secret_key,
|
"showSecretKey" => show_secret_key,
|
||||||
"bucketId" => bucket_id,
|
"bucketId" => bucket_id,
|
||||||
"key" => key
|
"key" => key,
|
||||||
|
"details" => details,
|
||||||
|
"offset" => offset,
|
||||||
|
"limit" => limit
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-228
@@ -47,26 +47,15 @@ where
|
|||||||
HI: Iterator<Item = S>,
|
HI: Iterator<Item = S>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
rule.allow_origins.iter().any(|x| wildcard_match(x, origin))
|
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
|
||||||
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
|
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
|
||||||
&& request_headers.all(|h| {
|
&& request_headers.all(|h| {
|
||||||
rule.allow_headers
|
rule.allow_headers
|
||||||
.iter()
|
.iter()
|
||||||
.any(|x| wildcard_match(x, h.as_ref()))
|
.any(|x| x == "*" || x == h.as_ref())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether `candidate` matches the pattern `allowed_wildcard`.
|
|
||||||
#[inline]
|
|
||||||
fn wildcard_match(allowed_wildcard: &String, candidate: &str) -> bool {
|
|
||||||
if allowed_wildcard.contains("*") {
|
|
||||||
let parts = allowed_wildcard.split("*").collect::<Vec<&str>>();
|
|
||||||
parts.len() == 2 && candidate.starts_with(parts[0]) && candidate.ends_with(parts[1])
|
|
||||||
} else {
|
|
||||||
candidate == allowed_wildcard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_cors_headers(
|
pub fn add_cors_headers(
|
||||||
resp: &mut Response<impl Body>,
|
resp: &mut Response<impl Body>,
|
||||||
rule: &GarageCorsRule,
|
rule: &GarageCorsRule,
|
||||||
@@ -201,221 +190,6 @@ pub fn handle_options_for_bucket<B>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn cors_rule(
|
|
||||||
allow_origins: &[&str],
|
|
||||||
allow_methods: &[&str],
|
|
||||||
allow_headers: &[&str],
|
|
||||||
) -> GarageCorsRule {
|
|
||||||
GarageCorsRule {
|
|
||||||
id: None,
|
|
||||||
max_age_seconds: None,
|
|
||||||
allow_origins: allow_origins.iter().map(|s| s.to_string()).collect(),
|
|
||||||
allow_methods: allow_methods.iter().map(|s| s.to_string()).collect(),
|
|
||||||
allow_headers: allow_headers.iter().map(|s| s.to_string()).collect(),
|
|
||||||
expose_headers: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn matches_when_origin_method_and_headers_are_explicitly_allowed() {
|
|
||||||
let rule = cors_rule(
|
|
||||||
&["https://app.example.test"],
|
|
||||||
&["GET", "PUT"],
|
|
||||||
&["content-type", "x-custom"],
|
|
||||||
);
|
|
||||||
let headers = vec!["content-type", "x-custom"];
|
|
||||||
|
|
||||||
assert!(cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://app.example.test",
|
|
||||||
"PUT",
|
|
||||||
headers.iter(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn does_not_match_when_origin_is_not_allowed() {
|
|
||||||
let rule = cors_rule(&["https://app.example.test"], &["GET"], &["*"]);
|
|
||||||
|
|
||||||
assert!(!cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://evil.example.test",
|
|
||||||
"GET",
|
|
||||||
std::iter::empty::<&str>(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn does_not_match_when_method_is_not_allowed() {
|
|
||||||
let rule = cors_rule(&["*"], &["GET"], &["*"]);
|
|
||||||
|
|
||||||
assert!(!cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://app.example.test",
|
|
||||||
"DELETE",
|
|
||||||
std::iter::empty::<&str>(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn does_not_match_when_a_requested_header_is_not_allowed() {
|
|
||||||
let rule = cors_rule(&["*"], &["GET"], &["content-type"]);
|
|
||||||
let headers = vec!["content-type", "x-not-allowed"];
|
|
||||||
|
|
||||||
assert!(!cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://app.example.test",
|
|
||||||
"GET",
|
|
||||||
headers.iter(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn wildcard_origin_method_and_headers_match_anything() {
|
|
||||||
let rule = cors_rule(&["*"], &["*"], &["*"]);
|
|
||||||
let headers = vec!["x-anything"];
|
|
||||||
|
|
||||||
assert!(cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://app.example.test",
|
|
||||||
"DELETE",
|
|
||||||
headers.iter(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn wildcard_origin_regex() {
|
|
||||||
let rule = cors_rule(&["https://*.localhost.com"], &["*"], &["*"]);
|
|
||||||
let headers = vec!["x-anything"];
|
|
||||||
|
|
||||||
assert!(cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://s3.localhost.com",
|
|
||||||
"DELETE",
|
|
||||||
headers.iter(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn origin_matching_cases() {
|
|
||||||
// (allow_origins, origin, expect_match)
|
|
||||||
let cases: &[(&[&str], &str, bool)] = &[
|
|
||||||
// exact match
|
|
||||||
(
|
|
||||||
&["https://app.example.test"],
|
|
||||||
"https://app.example.test",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&["https://app.example.test"],
|
|
||||||
"https://other.example.test",
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
// full wildcard
|
|
||||||
(&["*"], "https://anything.example.test", true),
|
|
||||||
// subdomain glob
|
|
||||||
(
|
|
||||||
&["https://*.example.test"],
|
|
||||||
"https://foo.example.test",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(&["https://*.example.test"], "https://example.test", false),
|
|
||||||
(
|
|
||||||
&["https://*.example.test"],
|
|
||||||
"http://foo.example.test",
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
// multiple allowed origins, at least one should match
|
|
||||||
(
|
|
||||||
&["https://a.example.test", "https://b.example.test"],
|
|
||||||
"https://b.example.test",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
// match multiple origins
|
|
||||||
(
|
|
||||||
&["https://a*.example.test", "https://ab*.example.test"],
|
|
||||||
"https://abc.example.test",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&["https://a.example.test", "https://b.example.test"],
|
|
||||||
"https://c.example.test",
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
// at most one '*' in a pattern is allowed
|
|
||||||
(&["https://*.example.*"], "https://a.example.test", false),
|
|
||||||
// domain changed with wildcard
|
|
||||||
(
|
|
||||||
&["https://*example.test"],
|
|
||||||
"https://garageexample.test",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
// trailing '*' matches any suffix, including the empty string,
|
|
||||||
// so this also matches origins with anything (or nothing) after
|
|
||||||
// "example."
|
|
||||||
(&["https://example.*"], "https://example.test", true),
|
|
||||||
(&["https://*example.test"], "https://example.test", true),
|
|
||||||
(&["https://example.*"], "https://example.", true),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (allow_origins, origin, expect_match) in cases {
|
|
||||||
let rule = cors_rule(allow_origins, &["GET"], &["*"]);
|
|
||||||
let got = cors_rule_matches(&rule, origin, "GET", std::iter::empty::<&str>());
|
|
||||||
assert_eq!(
|
|
||||||
got, *expect_match,
|
|
||||||
"allow_origins={allow_origins:?}, origin={origin:?}: expected match={expect_match}, got {got}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn header_matching_cases() {
|
|
||||||
// (allow_headers, requested_headers, expect_match)
|
|
||||||
let cases: &[(&[&str], &[&str], bool)] = &[
|
|
||||||
// exact match
|
|
||||||
(&["content-type"], &["content-type"], true),
|
|
||||||
(&["content-type"], &["x-custom"], false),
|
|
||||||
// full wildcard
|
|
||||||
(&["*"], &["x-anything"], true),
|
|
||||||
// no headers requested always matches, regardless of allow_headers
|
|
||||||
(&["content-type"], &[], true),
|
|
||||||
(&[], &[], true),
|
|
||||||
// prefix glob
|
|
||||||
(&["x-amz-*"], &["x-amz-meta-foo"], true),
|
|
||||||
(&["x-amz-*"], &["x-amz-"], true),
|
|
||||||
(&["x-amz-*"], &["x-other"], false),
|
|
||||||
// suffix glob
|
|
||||||
(&["*-meta"], &["foo-meta"], true),
|
|
||||||
(&["*-meta"], &["-meta"], true),
|
|
||||||
(&["*-meta"], &["foo-meta-bar"], false),
|
|
||||||
// multiple allowed headers, at least one should match per requested header
|
|
||||||
(
|
|
||||||
&["content-type", "x-amz-*"],
|
|
||||||
&["content-type", "x-amz-meta-foo"],
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(&["content-type", "x-amz-*"], &["x-other"], false),
|
|
||||||
// all requested headers must be covered
|
|
||||||
(&["content-type"], &["content-type", "x-custom"], false),
|
|
||||||
// at most one '*' in a pattern is allowed
|
|
||||||
(&["x-*-*"], &["x-a-b"], false),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (allow_headers, requested_headers, expect_match) in cases {
|
|
||||||
let rule = cors_rule(&["*"], &["GET"], allow_headers);
|
|
||||||
let got = cors_rule_matches(
|
|
||||||
&rule,
|
|
||||||
"https://app.example.test",
|
|
||||||
"GET",
|
|
||||||
requested_headers.iter(),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
got, *expect_match,
|
|
||||||
"allow_headers={allow_headers:?}, requested_headers={requested_headers:?}: expected match={expect_match}, got {got}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bucket_params_with_rule(allow_origins: Vec<&str>) -> BucketParams {
|
fn bucket_params_with_rule(allow_origins: Vec<&str>) -> BucketParams {
|
||||||
let mut bucket_params = BucketParams::default();
|
let mut bucket_params = BucketParams::default();
|
||||||
bucket_params.cors_config.update(
|
bucket_params.cors_config.update(
|
||||||
|
|||||||
@@ -31,12 +31,19 @@ impl Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_list_buckets(&self) -> Result<(), Error> {
|
pub async fn cmd_list_buckets(&self) -> Result<(), Error> {
|
||||||
let mut buckets = self.api_request(ListBucketsRequest).await?;
|
let mut buckets = match self.api_request(ListBucketsRequest::default()).await? {
|
||||||
|
ListBucketsResponse::WithoutDetails(list) => list,
|
||||||
|
_ => {
|
||||||
|
return Err(Error::Message(
|
||||||
|
"Unexpected ListBuckets response format".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
buckets.0.sort_by_key(|x| x.created);
|
buckets.sort_by_key(|x| x.created);
|
||||||
|
|
||||||
let mut table = vec!["ID\tCreated\tGlobal aliases\tLocal aliases".to_string()];
|
let mut table = vec!["ID\tCreated\tGlobal aliases\tLocal aliases".to_string()];
|
||||||
for bucket in buckets.0.iter() {
|
for bucket in buckets.iter() {
|
||||||
table.push(format!(
|
table.push(format!(
|
||||||
"{:.16}\t{}\t{}\t{}",
|
"{:.16}\t{}\t{}\t{}",
|
||||||
bucket.id,
|
bucket.id,
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ impl Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_list_keys(&self) -> Result<(), Error> {
|
pub async fn cmd_list_keys(&self) -> Result<(), Error> {
|
||||||
let mut keys = self.api_request(ListKeysRequest).await?;
|
let mut keys = match self.api_request(ListKeysRequest::default()).await? {
|
||||||
|
ListKeysResponse::WithoutDetails(list) => list,
|
||||||
|
_ => return Err(Error::Message("Unexpected ListKeys response format".into())),
|
||||||
|
};
|
||||||
|
|
||||||
keys.0.sort_by_key(|x| x.created);
|
keys.sort_by_key(|x| x.created);
|
||||||
|
|
||||||
let mut table = vec!["ID\tCreated\tName\tExpiration".to_string()];
|
let mut table = vec!["ID\tCreated\tName\tExpiration".to_string()];
|
||||||
for key in keys.0.iter() {
|
for key in keys.iter() {
|
||||||
let exp = if key.expired {
|
let exp = if key.expired {
|
||||||
Cow::from("expired")
|
Cow::from("expired")
|
||||||
} else {
|
} else {
|
||||||
@@ -243,7 +246,10 @@ impl Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_delete_expired_keys(&self, yes: bool) -> Result<(), Error> {
|
pub async fn cmd_delete_expired_keys(&self, yes: bool) -> Result<(), Error> {
|
||||||
let mut list = self.api_request(ListKeysRequest).await?.0;
|
let mut list = match self.api_request(ListKeysRequest::default()).await? {
|
||||||
|
ListKeysResponse::WithoutDetails(list) => list,
|
||||||
|
_ => return Err(Error::Message("Unexpected ListKeys response format".into())),
|
||||||
|
};
|
||||||
|
|
||||||
list.retain(|key| key.expired);
|
list.retain(|key| key.expired);
|
||||||
|
|
||||||
|
|||||||
@@ -110,16 +110,29 @@ impl Cli {
|
|||||||
Ok(resp.success.into_iter().next().unwrap().1)
|
Ok(resp.success.into_iter().next().unwrap().1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_json_api(&self, endpoint: String, payload: String) -> Result<(), Error> {
|
pub async fn cmd_json_api(
|
||||||
let payload: serde_json::Value = if payload == "-" {
|
&self,
|
||||||
serde_json::from_reader(&std::io::stdin())?
|
endpoint: String,
|
||||||
} else {
|
payload: Option<String>,
|
||||||
serde_json::from_str(&payload)?
|
) -> Result<(), Error> {
|
||||||
};
|
let request: AdminApiRequest = if let Some(payload) = payload {
|
||||||
|
let payload: serde_json::Value = if payload == "-" {
|
||||||
|
serde_json::from_reader(&std::io::stdin())?
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(&payload)?
|
||||||
|
};
|
||||||
|
|
||||||
let request: AdminApiRequest = serde_json::from_value(serde_json::json!({
|
serde_json::from_value(serde_json::json!({
|
||||||
endpoint.clone(): payload,
|
endpoint.clone(): payload,
|
||||||
}))?;
|
}))?
|
||||||
|
} else {
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
endpoint.clone(): null,
|
||||||
|
}))
|
||||||
|
.or(serde_json::from_value(serde_json::json!({
|
||||||
|
endpoint.clone(): {},
|
||||||
|
})))?
|
||||||
|
};
|
||||||
|
|
||||||
let resp = match self
|
let resp = match self
|
||||||
.proxy_rpc_endpoint
|
.proxy_rpc_endpoint
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ pub enum Command {
|
|||||||
/// The admin API endpoint to invoke, e.g. `GetClusterStatus`
|
/// The admin API endpoint to invoke, e.g. `GetClusterStatus`
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
/// The JSON payload, or `-` to read from `stdin`
|
/// The JSON payload, or `-` to read from `stdin`
|
||||||
#[structopt(default_value = "null")]
|
payload: Option<String>,
|
||||||
payload: String,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Generate completions for a shell
|
/// Generate completions for a shell
|
||||||
|
|||||||
@@ -276,11 +276,6 @@ fn init_logging(opt: &Opt) {
|
|||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.with_env_filter(env_filter)
|
.with_env_filter(env_filter)
|
||||||
.with_ansi(
|
|
||||||
std::env::var("NO_COLOR")
|
|
||||||
.map(|x| x != "0" && !x.eq_ignore_ascii_case("false"))
|
|
||||||
.unwrap_or(true),
|
|
||||||
)
|
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,14 +152,6 @@ pub async fn run_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deregister from Consul (if enabled) in the background, in parallel with the
|
|
||||||
// rest of the shutdown sequence, so that it doesn't add to shutdown latency.
|
|
||||||
#[cfg(feature = "consul-discovery")]
|
|
||||||
let deregister_consul_task = tokio::spawn({
|
|
||||||
let system = garage.system.clone();
|
|
||||||
async move { system.deregister_from_discovery().await }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove RPC handlers for system to break reference cycles
|
// Remove RPC handlers for system to break reference cycles
|
||||||
info!("Deregistering RPC handlers for shutdown...");
|
info!("Deregistering RPC handlers for shutdown...");
|
||||||
garage.system.netapp.drop_all_handlers();
|
garage.system.netapp.drop_all_handlers();
|
||||||
@@ -176,12 +168,6 @@ pub async fn run_server(
|
|||||||
// Await for all background tasks to end
|
// Await for all background tasks to end
|
||||||
await_background_done.await?;
|
await_background_done.await?;
|
||||||
|
|
||||||
// Await for Consul deregistration to end, if it hasn't already
|
|
||||||
#[cfg(feature = "consul-discovery")]
|
|
||||||
if let Err(e) = deregister_consul_task.await {
|
|
||||||
error!("Error while joining Consul deregistration task: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Cleaning up...");
|
info!("Cleaning up...");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -183,36 +183,6 @@ impl ConsulDiscovery {
|
|||||||
}
|
}
|
||||||
// ---- PUBLISHING TO CONSUL CATALOG ----
|
// ---- PUBLISHING TO CONSUL CATALOG ----
|
||||||
|
|
||||||
#[cfg(feature = "consul-discovery")]
|
|
||||||
pub async fn deregister_consul_service(&self, node_id: NodeID) -> Result<(), ConsulError> {
|
|
||||||
let node = format!("garage:{}", hex::encode(&node_id[..8]));
|
|
||||||
let url = format!(
|
|
||||||
"{}/v1/{}",
|
|
||||||
self.config.consul_http_addr,
|
|
||||||
(match &self.config.api {
|
|
||||||
ConsulDiscoveryAPI::Catalog => format!("catalog/deregister"),
|
|
||||||
ConsulDiscoveryAPI::Agent => format!("agent/service/deregister/{}", node),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let req = self.client.put(&url);
|
|
||||||
|
|
||||||
let http = if matches!(&self.config.api, ConsulDiscoveryAPI::Catalog) {
|
|
||||||
let deregister_request = serde_json::json!({
|
|
||||||
"Node": node,
|
|
||||||
"ServiceID": node,
|
|
||||||
});
|
|
||||||
let req = req.json(&deregister_request);
|
|
||||||
req.send().await?
|
|
||||||
} else {
|
|
||||||
req.send().await?
|
|
||||||
};
|
|
||||||
http.error_for_status()?;
|
|
||||||
|
|
||||||
debug!("Deregistered service {} from Consul", node);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_consul_service(
|
pub async fn publish_consul_service(
|
||||||
&self,
|
&self,
|
||||||
node_id: NodeID,
|
node_id: NodeID,
|
||||||
|
|||||||
+1
-10
@@ -358,7 +358,7 @@ impl System {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cleanup(self: &Arc<Self>) {
|
pub fn cleanup(&self) {
|
||||||
// Break reference cycle
|
// Break reference cycle
|
||||||
self.metrics.store(None);
|
self.metrics.store(None);
|
||||||
}
|
}
|
||||||
@@ -650,15 +650,6 @@ impl System {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "consul-discovery")]
|
|
||||||
pub async fn deregister_from_discovery(self: &Arc<Self>) {
|
|
||||||
if let Some(c) = &self.consul_discovery {
|
|
||||||
if let Err(e) = c.deregister_consul_service(self.netapp.id).await {
|
|
||||||
error!("Error while deregistering from Consul: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn discovery_loop(self: &Arc<Self>, mut stop_signal: watch::Receiver<bool>) {
|
async fn discovery_loop(self: &Arc<Self>, mut stop_signal: watch::Receiver<bool>) {
|
||||||
while !*stop_signal.borrow() {
|
while !*stop_signal.borrow() {
|
||||||
let peers_up = self
|
let peers_up = self
|
||||||
|
|||||||
Reference in New Issue
Block a user