aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/garage-admin-v0.yml6
-rw-r--r--doc/book/connect/_index.md7
-rw-r--r--doc/book/connect/apps/index.md65
-rw-r--r--doc/book/connect/observability.md57
-rw-r--r--src/api/admin/api_server.rs48
-rw-r--r--src/api/admin/router.rs3
-rw-r--r--src/garage/tests/common/garage.rs2
-rw-r--r--src/garage/tests/s3/website.rs136
8 files changed, 319 insertions, 5 deletions
diff --git a/doc/api/garage-admin-v0.yml b/doc/api/garage-admin-v0.yml
index a841f8d9..51968894 100644
--- a/doc/api/garage-admin-v0.yml
+++ b/doc/api/garage-admin-v0.yml
@@ -678,10 +678,12 @@ paths:
properties:
maxSize:
type: integer
+ format: int64
nullable: true
example: 19029801
maxObjects:
type: integer
+ format: int64
nullable: true
example: null
@@ -1158,9 +1160,11 @@ components:
$ref: '#/components/schemas/BucketKeyInfo'
objects:
type: integer
+ format: int64
example: 14827
bytes:
type: integer
+ format: int64
example: 13189855625
unfinishedUploads:
type: integer
@@ -1171,10 +1175,12 @@ components:
maxSize:
nullable: true
type: integer
+ format: int64
example: null
maxObjects:
nullable: true
type: integer
+ format: int64
example: null
diff --git a/doc/book/connect/_index.md b/doc/book/connect/_index.md
index ca44ac17..93a2b87e 100644
--- a/doc/book/connect/_index.md
+++ b/doc/book/connect/_index.md
@@ -10,11 +10,12 @@ Garage implements the Amazon S3 protocol, which makes it compatible with many ex
In particular, you will find here instructions to connect it with:
- - [Browsing tools](@/documentation/connect/cli.md)
- [Applications](@/documentation/connect/apps/index.md)
- - [Website hosting](@/documentation/connect/websites.md)
- - [Software repositories](@/documentation/connect/repositories.md)
+ - [Browsing tools](@/documentation/connect/cli.md)
- [FUSE](@/documentation/connect/fs.md)
+ - [Observability](@/documentation/connect/observability.md)
+ - [Software repositories](@/documentation/connect/repositories.md)
+ - [Website hosting](@/documentation/connect/websites.md)
### Generic instructions
diff --git a/doc/book/connect/apps/index.md b/doc/book/connect/apps/index.md
index 737351a0..4d556ff8 100644
--- a/doc/book/connect/apps/index.md
+++ b/doc/book/connect/apps/index.md
@@ -13,7 +13,7 @@ In this section, we cover the following web applications:
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
| [Pixelfed](#pixelfed) | ❓ | Not yet tested |
| [Pleroma](#pleroma) | ❓ | Not yet tested |
-| [Lemmy](#lemmy) | ❓ | Not yet tested |
+| [Lemmy](#lemmy) | ✅ | Supported with pict-rs |
| [Funkwhale](#funkwhale) | ❓ | Not yet tested |
| [Misskey](#misskey) | ❓ | Not yet tested |
| [Prismo](#prismo) | ❓ | Not yet tested |
@@ -484,7 +484,68 @@ And add a new line. For example, to run it every 10 minutes:
## Lemmy
-Lemmy uses pict-rs that [supports S3 backends](https://git.asonix.dog/asonix/pict-rs/commit/f9f4fc63d670f357c93f24147c2ee3e1278e2d97)
+Lemmy uses pict-rs that [supports S3 backends](https://git.asonix.dog/asonix/pict-rs/commit/f9f4fc63d670f357c93f24147c2ee3e1278e2d97).
+This feature requires `pict-rs >= 4.0.0`.
+
+### Creating your bucket
+
+This is the usual Garage setup:
+
+```bash
+garage key new --name pictrs-key
+garage bucket create pictrs-data
+garage bucket allow pictrs-data --read --write --key pictrs-key
+```
+
+Note the Key ID and Secret Key.
+
+### Migrating your data
+
+If your pict-rs instance holds existing data, you first need to migrate to the S3 bucket.
+
+Stop pict-rs, then run the migration utility from local filesystem to the bucket:
+
+```
+pict-rs \
+ filesystem -p /path/to/existing/files \
+ object-store \
+ -e my-garage-instance.mydomain.tld:3900 \
+ -b pictrs-data \
+ -r garage \
+ -a GK... \
+ -s abcdef0123456789...
+```
+
+This is pretty slow, so hold on while migrating.
+
+### Running pict-rs with an S3 backend
+
+Pict-rs supports both a configuration file and environment variables.
+
+Either set the following section in your `pict-rs.toml`:
+
+```
+[store]
+type = 'object_storage'
+endpoint = 'http://my-garage-instance.mydomain.tld:3900'
+bucket_name = 'pictrs-data'
+region = 'garage'
+access_key = 'GK...'
+secret_key = 'abcdef0123456789...'
+```
+
+... or set these environment variables:
+
+
+```
+PICTRS__STORE__TYPE=object_storage
+PICTRS__STORE__ENDPOINT=http:/my-garage-instance.mydomain.tld:3900
+PICTRS__STORE__BUCKET_NAME=pictrs-data
+PICTRS__STORE__REGION=garage
+PICTRS__STORE__ACCESS_KEY=GK...
+PICTRS__STORE__SECRET_KEY=abcdef0123456789...
+```
+
## Funkwhale
diff --git a/doc/book/connect/observability.md b/doc/book/connect/observability.md
new file mode 100644
index 00000000..c5037fa4
--- /dev/null
+++ b/doc/book/connect/observability.md
@@ -0,0 +1,57 @@
++++
+title = "Observability"
+weight = 25
++++
+
+An object store can be used as data storage location for metrics, and logs which
+can then be leveraged for systems observability.
+
+## Metrics
+
+### Prometheus
+
+Prometheus itself has no object store capabilities, however two projects exist
+which support storing metrics in an object store:
+
+ - [Cortex](https://cortexmetrics.io/)
+ - [Thanos](https://thanos.io/)
+
+## System logs
+
+### Vector
+
+[Vector](https://vector.dev/) natively supports S3 as a
+[data sink](https://vector.dev/docs/reference/configuration/sinks/aws_s3/)
+(and [source](https://vector.dev/docs/reference/configuration/sources/aws_s3/)).
+
+This can be configured with Garage with the following:
+
+```bash
+garage key new --name vector-system-logs
+garage bucket create system-logs
+garage bucket allow system-logs --read --write --key vector-system-logs
+```
+
+The `vector.toml` can then be configured as follows:
+
+```toml
+[sources.journald]
+type = "journald"
+current_boot_only = true
+
+[sinks.out]
+encoding.codec = "json"
+type = "aws_s3"
+inputs = [ "journald" ]
+bucket = "system-logs"
+key_prefix = "%F/"
+compression = "none"
+region = "garage"
+endpoint = "https://my-garage-instance.mydomain.tld"
+auth.access_key_id = ""
+auth.secret_access_key = ""
+```
+
+This is an example configuration - please refer to the Vector documentation for
+all configuration and transformation possibilities. Also note that Garage
+performs its own compression, so this should be disabled in Vector.
diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs
index 2d325fb1..7a534f32 100644
--- a/src/api/admin/api_server.rs
+++ b/src/api/admin/api_server.rs
@@ -77,6 +77,53 @@ impl AdminApiServer {
.body(Body::empty())?)
}
+ async fn handle_check_website_enabled(
+ &self,
+ req: Request<Body>,
+ ) -> Result<Response<Body>, Error> {
+ let has_domain_header = req.headers().contains_key("domain");
+
+ if !has_domain_header {
+ return Err(Error::bad_request("No domain header found"));
+ }
+
+ let domain = &req
+ .headers()
+ .get("domain")
+ .ok_or_internal_error("Could not parse domain header")?;
+
+ let domain_string = String::from(
+ domain
+ .to_str()
+ .ok_or_bad_request("Invalid characters found in domain header")?,
+ );
+
+ let bucket_id = self
+ .garage
+ .bucket_helper()
+ .resolve_global_bucket_name(&domain_string)
+ .await?
+ .ok_or_else(|| HelperError::NoSuchBucket(domain_string))?;
+
+ let bucket = self
+ .garage
+ .bucket_helper()
+ .get_existing_bucket(bucket_id)
+ .await?;
+
+ let bucket_state = bucket.state.as_option().unwrap();
+ let bucket_website_config = bucket_state.website_config.get();
+
+ match bucket_website_config {
+ Some(_v) => Ok(Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from("Bucket authorized for website hosting"))?),
+ None => Err(Error::bad_request(
+ "Bucket is not authorized for website hosting",
+ )),
+ }
+ }
+
fn handle_health(&self) -> Result<Response<Body>, Error> {
let health = self.garage.system.health();
@@ -174,6 +221,7 @@ impl ApiHandler for AdminApiServer {
match endpoint {
Endpoint::Options => self.handle_options(&req),
+ Endpoint::CheckWebsiteEnabled => self.handle_check_website_enabled(req).await,
Endpoint::Health => self.handle_health(),
Endpoint::Metrics => self.handle_metrics(),
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs
index 62e6abc3..0dcb1546 100644
--- a/src/api/admin/router.rs
+++ b/src/api/admin/router.rs
@@ -17,6 +17,7 @@ router_match! {@func
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint {
Options,
+ CheckWebsiteEnabled,
Health,
Metrics,
GetClusterStatus,
@@ -91,6 +92,7 @@ impl Endpoint {
let res = router_match!(@gen_path_parser (req.method(), path, query) [
OPTIONS _ => Options,
+ GET "/check" => CheckWebsiteEnabled,
GET "/health" => Health,
GET "/metrics" => Metrics,
GET "/v0/status" => GetClusterStatus,
@@ -136,6 +138,7 @@ impl Endpoint {
pub fn authorization_type(&self) -> Authorization {
match self {
Self::Health => Authorization::None,
+ Self::CheckWebsiteEnabled => Authorization::None,
Self::Metrics => Authorization::MetricsToken,
_ => Authorization::AdminToken,
}
diff --git a/src/garage/tests/common/garage.rs b/src/garage/tests/common/garage.rs
index 44d727f9..8f994f49 100644
--- a/src/garage/tests/common/garage.rs
+++ b/src/garage/tests/common/garage.rs
@@ -25,6 +25,7 @@ pub struct Instance {
pub s3_port: u16,
pub k2v_port: u16,
pub web_port: u16,
+ pub admin_port: u16,
}
impl Instance {
@@ -105,6 +106,7 @@ api_bind_addr = "127.0.0.1:{admin_port}"
s3_port: port,
k2v_port: port + 1,
web_port: port + 3,
+ admin_port: port + 4,
}
}
diff --git a/src/garage/tests/s3/website.rs b/src/garage/tests/s3/website.rs
index 244a2fa0..f57e31ee 100644
--- a/src/garage/tests/s3/website.rs
+++ b/src/garage/tests/s3/website.rs
@@ -1,5 +1,8 @@
use crate::common;
use crate::common::ext::*;
+use crate::k2v::json_body;
+
+use assert_json_diff::assert_json_eq;
use aws_sdk_s3::{
model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
types::ByteStream,
@@ -9,6 +12,7 @@ use hyper::{
body::{to_bytes, Body},
Client,
};
+use serde_json::json;
const BODY: &[u8; 16] = b"<h1>bonjour</h1>";
const BODY_ERR: &[u8; 6] = b"erreur";
@@ -49,6 +53,28 @@ async fn test_website() {
BODY.as_ref()
); /* check that we do not leak body */
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .header("domain", format!("{}", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
+ let res_body = json_body(admin_resp).await;
+ assert_json_eq!(
+ res_body,
+ json!({
+ "code": "InvalidRequest",
+ "message": "Bad request: Bucket is not authorized for website hosting",
+ "region": "garage-integ-test",
+ "path": "/check",
+ })
+ );
+
ctx.garage
.command()
.args(["bucket", "website", "--allow", BCKT_NAME])
@@ -62,6 +88,22 @@ async fn test_website() {
BODY.as_ref()
);
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .header("domain", format!("{}", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let mut admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::OK);
+ assert_eq!(
+ to_bytes(admin_resp.body_mut()).await.unwrap().as_ref(),
+ b"Bucket authorized for website hosting"
+ );
+
ctx.garage
.command()
.args(["bucket", "website", "--deny", BCKT_NAME])
@@ -74,6 +116,28 @@ async fn test_website() {
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
); /* check that we do not leak body */
+
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .header("domain", format!("{}", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
+ let res_body = json_body(admin_resp).await;
+ assert_json_eq!(
+ res_body,
+ json!({
+ "code": "InvalidRequest",
+ "message": "Bad request: Bucket is not authorized for website hosting",
+ "region": "garage-integ-test",
+ "path": "/check",
+ })
+ );
}
#[tokio::test]
@@ -322,3 +386,75 @@ async fn test_website_s3_api() {
);
}
}
+
+#[tokio::test]
+async fn test_website_check_website_enabled() {
+ let ctx = common::context();
+
+ let client = Client::new();
+
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
+ let res_body = json_body(admin_resp).await;
+ assert_json_eq!(
+ res_body,
+ json!({
+ "code": "InvalidRequest",
+ "message": "Bad request: No domain header found",
+ "region": "garage-integ-test",
+ "path": "/check",
+ })
+ );
+
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .header("domain", "foobar")
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::NOT_FOUND);
+ let res_body = json_body(admin_resp).await;
+ assert_json_eq!(
+ res_body,
+ json!({
+ "code": "NoSuchBucket",
+ "message": "Bucket not found: foobar",
+ "region": "garage-integ-test",
+ "path": "/check",
+ })
+ );
+
+ let admin_req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
+ .header("domain", "☹")
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let admin_resp = client.request(admin_req()).await.unwrap();
+ assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
+ let res_body = json_body(admin_resp).await;
+ assert_json_eq!(
+ res_body,
+ json!({
+ "code": "InvalidRequest",
+ "message": "Bad request: Invalid characters found in domain header: failed to convert header to a str",
+ "region": "garage-integ-test",
+ "path": "/check",
+ })
+ );
+}