Merge branch 'main' into optimal-layout

This commit is contained in:
Alex Auvolat
2022-11-07 12:20:59 +01:00
40 changed files with 3593 additions and 2600 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ sha2 = "0.10"
futures = "0.3"
futures-util = "0.3"
pin-project = "1.0"
pin-project = "1.0.11"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-stream = "0.1"
+3 -3
View File
@@ -5,7 +5,7 @@ use async_trait::async_trait;
use futures::future::Future;
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
use hyper::{Body, Request, Response};
use hyper::{Body, Request, Response, StatusCode};
use opentelemetry::trace::SpanRef;
@@ -69,7 +69,7 @@ impl AdminApiServer {
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
Ok(Response::builder()
.status(204)
.status(StatusCode::NO_CONTENT)
.header(ALLOW, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
@@ -94,7 +94,7 @@ impl AdminApiServer {
.ok_or_internal_error("Could not serialize metrics")?;
Ok(Response::builder()
.status(200)
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))?)
}
+3 -3
View File
@@ -151,7 +151,7 @@ pub async fn handle_update_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
@@ -171,7 +171,7 @@ pub async fn handle_apply_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
@@ -186,7 +186,7 @@ pub async fn handle_revert_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub async fn handle_insert_batch(
garage.k2v.rpc.insert_batch(bucket_id, items2).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+1 -1
View File
@@ -153,7 +153,7 @@ pub async fn handle_insert_item(
.await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+9 -4
View File
@@ -1,9 +1,5 @@
use crate::*;
use crate::lmdb_adapter::LmdbDb;
use crate::sled_adapter::SledDb;
use crate::sqlite_adapter::SqliteDb;
fn test_suite(db: Db) {
let tree = db.open_tree("tree").unwrap();
@@ -80,7 +76,10 @@ fn test_suite(db: Db) {
}
#[test]
#[cfg(feature = "lmdb")]
fn test_lmdb_db() {
use crate::lmdb_adapter::LmdbDb;
let path = mktemp::Temp::new_dir().unwrap();
let db = heed::EnvOpenOptions::new()
.max_dbs(100)
@@ -92,7 +91,10 @@ fn test_lmdb_db() {
}
#[test]
#[cfg(feature = "sled")]
fn test_sled_db() {
use crate::sled_adapter::SledDb;
let path = mktemp::Temp::new_dir().unwrap();
let db = SledDb::init(sled::open(path.to_path_buf()).unwrap());
test_suite(db);
@@ -100,7 +102,10 @@ fn test_sled_db() {
}
#[test]
#[cfg(feature = "sqlite")]
fn test_sqlite_db() {
use crate::sqlite_adapter::SqliteDb;
let db = SqliteDb::init(rusqlite::Connection::open_in_memory().unwrap());
test_suite(db);
}
+5 -1
View File
@@ -30,9 +30,11 @@ garage_table = { version = "0.8.0", path = "../table" }
garage_util = { version = "0.8.0", path = "../util" }
garage_web = { version = "0.8.0", path = "../web" }
backtrace = "0.3"
bytes = "1.0"
bytesize = "1.1"
timeago = "0.3"
parse_duration = "2.1"
hex = "0.4"
tracing = { version = "0.1.30", features = ["log-always"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -58,7 +60,7 @@ opentelemetry-otlp = { version = "0.10", optional = true }
prometheus = { version = "0.13", optional = true }
[dev-dependencies]
aws-sdk-s3 = "0.8"
aws-sdk-s3 = "0.19"
chrono = "0.4"
http = "0.2"
hmac = "0.12"
@@ -81,6 +83,8 @@ sled = [ "garage_model/sled" ]
lmdb = [ "garage_model/lmdb" ]
sqlite = [ "garage_model/sqlite" ]
# Automatic registration and discovery via Consul API
consul-discovery = [ "garage_rpc/consul-discovery" ]
# Automatic registration and discovery via Kubernetes API
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
# Prometheus exporter (/metrics endpoint).
+39
View File
@@ -85,6 +85,9 @@ impl AdminRpcHandler {
BucketOperation::Deny(query) => self.handle_bucket_deny(query).await,
BucketOperation::Website(query) => self.handle_bucket_website(query).await,
BucketOperation::SetQuotas(query) => self.handle_bucket_set_quotas(query).await,
BucketOperation::CleanupIncompleteUploads(query) => {
self.handle_bucket_cleanup_incomplete_uploads(query).await
}
}
}
@@ -512,6 +515,42 @@ impl AdminRpcHandler {
)))
}
async fn handle_bucket_cleanup_incomplete_uploads(
&self,
query: &CleanupIncompleteUploadsOpt,
) -> Result<AdminRpc, Error> {
let mut bucket_ids = vec![];
for b in query.buckets.iter() {
bucket_ids.push(
self.garage
.bucket_helper()
.resolve_global_bucket_name(b)
.await?
.ok_or_bad_request(format!("Bucket not found: {}", b))?,
);
}
let duration = parse_duration::parse::parse(&query.older_than)
.ok_or_bad_request("Invalid duration passed for --older-than parameter")?;
let mut ret = String::new();
for bucket in bucket_ids {
let count = self
.garage
.bucket_helper()
.cleanup_incomplete_uploads(&bucket, duration)
.await?;
writeln!(
&mut ret,
"Bucket {:?}: {} incomplete uploads aborted",
bucket, count
)
.unwrap();
}
Ok(AdminRpc::Ok(ret))
}
async fn handle_key_cmd(&self, cmd: &KeyOperation) -> Result<AdminRpc, Error> {
match cmd {
KeyOperation::List => self.handle_list_keys().await,
+15
View File
@@ -200,6 +200,10 @@ pub enum BucketOperation {
/// Set the quotas for this bucket
#[structopt(name = "set-quotas", version = garage_version())]
SetQuotas(SetQuotasOpt),
/// Clean up (abort) old incomplete multipart uploads
#[structopt(name = "cleanup-incomplete-uploads", version = garage_version())]
CleanupIncompleteUploads(CleanupIncompleteUploadsOpt),
}
#[derive(Serialize, Deserialize, StructOpt, Debug)]
@@ -301,6 +305,17 @@ pub struct SetQuotasOpt {
pub max_objects: Option<String>,
}
#[derive(Serialize, Deserialize, StructOpt, Debug)]
pub struct CleanupIncompleteUploadsOpt {
/// Abort multipart uploads older than this value
#[structopt(long = "older-than", default_value = "1d")]
pub older_than: String,
/// Name of bucket(s) to clean up
#[structopt(required = true)]
pub buckets: Vec<String>,
}
#[derive(Serialize, Deserialize, StructOpt, Debug)]
pub enum KeyOperation {
/// List keys
+42 -16
View File
@@ -65,21 +65,6 @@ struct Opt {
#[tokio::main]
async fn main() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "netapp=info,garage=info")
}
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();
sodiumoxide::init().expect("Unable to init sodiumoxide");
// Abort on panic (same behavior as in Go)
std::panic::set_hook(Box::new(|panic_info| {
error!("{}", panic_info.to_string());
std::process::abort();
}));
// Initialize version and features info
let features = &[
#[cfg(feature = "k2v")]
@@ -90,6 +75,8 @@ async fn main() {
"lmdb",
#[cfg(feature = "sqlite")]
"sqlite",
#[cfg(feature = "consul-discovery")]
"consul-discovery",
#[cfg(feature = "kubernetes-discovery")]
"kubernetes-discovery",
#[cfg(feature = "metrics")]
@@ -106,12 +93,51 @@ async fn main() {
}
garage_util::version::init_features(features);
// Parse arguments
let version = format!(
"{} [features: {}]",
garage_util::version::garage_version(),
features.join(", ")
);
// Initialize panic handler that aborts on panic and shows a nice message.
// By default, Tokio continues runing normally when a task panics. We want
// to avoid this behavior in Garage as this would risk putting the process in an
// unknown/uncontrollable state. We prefer to exit the process and restart it
// from scratch, so that it boots back into a fresh, known state.
let panic_version_info = version.clone();
std::panic::set_hook(Box::new(move |panic_info| {
eprintln!("======== PANIC (internal Garage error) ========");
eprintln!("{}", panic_info);
eprintln!();
eprintln!("Panics are internal errors that Garage is unable to handle on its own.");
eprintln!("They can be caused by bugs in Garage's code, or by corrupted data in");
eprintln!("the node's storage. If you feel that this error is likely to be a bug");
eprintln!("in Garage, please report it on our issue tracker a the following address:");
eprintln!();
eprintln!(" https://git.deuxfleurs.fr/Deuxfleurs/garage/issues");
eprintln!();
eprintln!("Please include the last log messages and the the full backtrace below in");
eprintln!("your bug report, as well as any relevant information on the context in");
eprintln!("which Garage was running when this error occurred.");
eprintln!();
eprintln!("GARAGE VERSION: {}", panic_version_info);
eprintln!();
eprintln!("BACKTRACE:");
eprintln!("{:?}", backtrace::Backtrace::new());
std::process::abort();
}));
// Initialize logging as well as other libraries used in Garage
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "netapp=info,garage=info")
}
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();
sodiumoxide::init().expect("Unable to init sodiumoxide");
// Parse arguments and dispatch command line
let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches());
let res = match opt.cmd {
+11 -11
View File
@@ -6,7 +6,7 @@ use assert_json_diff::assert_json_eq;
use serde_json::json;
use super::json_body;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_batch() {
@@ -49,7 +49,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
for sk in ["a", "b", "c", "d.1", "d.2", "e"] {
let res = ctx
@@ -62,7 +62,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -104,7 +104,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -266,7 +266,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
for sk in ["b", "c", "d.1", "d.2"] {
let res = ctx
@@ -280,9 +280,9 @@ async fn test_batch() {
.await
.unwrap();
if sk == "b" {
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
} else {
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
}
ct.insert(
sk,
@@ -317,7 +317,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -478,7 +478,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -514,7 +514,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -547,7 +547,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
+10 -10
View File
@@ -1,13 +1,13 @@
use crate::common;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_error_codes() {
let ctx = common::context();
let bucket = ctx.create_bucket("test-k2v-error-codes");
// Regular insert should work (code 200)
// Regular insert should work (code 204)
let res = ctx
.k2v
.request
@@ -19,7 +19,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Insert with trash causality token: invalid request
let res = ctx
@@ -34,7 +34,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search without partition key: invalid request
let res = ctx
@@ -52,7 +52,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search with start that is not in prefix: invalid request
let res = ctx
@@ -70,7 +70,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search with invalid json: 400
let res = ctx
@@ -88,7 +88,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Batch insert with invalid causality token: 400
let res = ctx
@@ -105,7 +105,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Batch insert with invalid data: 400
let res = ctx
@@ -122,7 +122,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Poll with invalid causality token: 400
let res = ctx
@@ -137,5 +137,5 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
+29 -29
View File
@@ -6,7 +6,7 @@ use assert_json_diff::assert_json_eq;
use serde_json::json;
use super::json_body;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_items_and_indices() {
@@ -56,7 +56,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -69,7 +69,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -132,7 +132,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -145,7 +145,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -201,7 +201,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -214,7 +214,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -271,7 +271,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let ct = res
.headers()
.get("x-garage-causality-token")
@@ -292,7 +292,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// ReadIndex -- now there should be some stuff
tokio::time::sleep(Duration::from_secs(1)).await;
@@ -364,7 +364,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -377,7 +377,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -405,7 +405,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -424,7 +424,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -446,7 +446,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -466,7 +466,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -479,7 +479,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -503,7 +503,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -528,7 +528,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 409); // CONFLICT
assert_eq!(res.status(), StatusCode::CONFLICT); // CONFLICT
// f3: json
let res = ctx
@@ -541,7 +541,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -568,7 +568,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -581,7 +581,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -599,7 +599,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -625,7 +625,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 409); // CONFLICT
assert_eq!(res.status(), StatusCode::CONFLICT); // CONFLICT
// f3: json
let res = ctx
@@ -638,7 +638,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -658,7 +658,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -671,7 +671,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204); // NO CONTENT
assert_eq!(res.status(), StatusCode::NO_CONTENT); // NO CONTENT
// f1: not specified
let res = ctx
@@ -683,7 +683,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -702,7 +702,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204); // NO CONTENT
assert_eq!(res.status(), StatusCode::NO_CONTENT); // NO CONTENT
// f3: json
let res = ctx
@@ -715,7 +715,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
+5 -5
View File
@@ -1,4 +1,4 @@
use hyper::Method;
use hyper::{Method, StatusCode};
use std::time::Duration;
use crate::common;
@@ -20,7 +20,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Retrieve initial value to get its causality token
let res2 = ctx
@@ -33,7 +33,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res2.status(), 200);
assert_eq!(res2.status(), StatusCode::OK);
let ct = res2
.headers()
.get("x-garage-causality-token")
@@ -80,7 +80,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Check poll finishes with correct value
let poll_res = tokio::select! {
@@ -88,7 +88,7 @@ async fn test_poll() {
res = poll => res.unwrap().unwrap(),
};
assert_eq!(poll_res.status(), 200);
assert_eq!(poll_res.status(), StatusCode::OK);
let poll_res_body = hyper::body::to_bytes(poll_res.into_body())
.await
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::common;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_simple() {
@@ -18,7 +18,7 @@ async fn test_simple() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
let res2 = ctx
.k2v
@@ -30,7 +30,7 @@ async fn test_simple() {
.send()
.await
.unwrap();
assert_eq!(res2.status(), 200);
assert_eq!(res2.status(), StatusCode::OK);
let res2_body = hyper::body::to_bytes(res2.into_body())
.await
+10 -10
View File
@@ -4,7 +4,7 @@ use aws_sdk_s3::{
model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
types::ByteStream,
};
use http::Request;
use http::{Request, StatusCode};
use hyper::{
body::{to_bytes, Body},
Client,
@@ -43,7 +43,7 @@ async fn test_website() {
let mut resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -56,7 +56,7 @@ async fn test_website() {
.expect_success_status("Could not allow website on bucket");
resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -69,7 +69,7 @@ async fn test_website() {
.expect_success_status("Could not deny website on bucket");
resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -175,7 +175,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
@@ -200,7 +200,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_eq!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY_ERR.as_ref()
@@ -220,7 +220,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
@@ -244,7 +244,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 403);
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -285,7 +285,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 403);
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -311,7 +311,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY_ERR.as_ref()
+1 -1
View File
@@ -12,7 +12,7 @@ readme = "../../README.md"
base64 = "0.13.0"
http = "0.2.6"
log = "0.4"
rusoto_core = "0.48.0"
rusoto_core = { version = "0.48.0", default-features = false, features = ["rustls"] }
rusoto_credential = "0.48.0"
rusoto_signature = "0.48.0"
serde = "1.0.137"
+68 -1
View File
@@ -1,3 +1,5 @@
use std::time::Duration;
use garage_util::crdt::*;
use garage_util::data::*;
use garage_util::error::{Error as GarageError, OkOrMessage};
@@ -12,7 +14,7 @@ use crate::helper::error::*;
use crate::helper::key::KeyHelper;
use crate::key_table::*;
use crate::permission::BucketKeyPerm;
use crate::s3::object_table::ObjectFilter;
use crate::s3::object_table::*;
pub struct BucketHelper<'a>(pub(crate) &'a Garage);
@@ -472,4 +474,69 @@ impl<'a> BucketHelper<'a> {
Ok(true)
}
// ----
/// Deletes all incomplete multipart uploads that are older than a certain time.
/// Returns the number of uploads aborted
pub async fn cleanup_incomplete_uploads(
&self,
bucket_id: &Uuid,
older_than: Duration,
) -> Result<usize, Error> {
let older_than = now_msec() - older_than.as_millis() as u64;
let mut ret = 0usize;
let mut start = None;
loop {
let objects = self
.0
.object_table
.get_range(
bucket_id,
start,
Some(ObjectFilter::IsUploading),
1000,
EnumerationOrder::Forward,
)
.await?;
let abortions = objects
.iter()
.filter_map(|object| {
let aborted_versions = object
.versions()
.iter()
.filter(|v| v.is_uploading() && v.timestamp < older_than)
.map(|v| ObjectVersion {
state: ObjectVersionState::Aborted,
uuid: v.uuid,
timestamp: v.timestamp,
})
.collect::<Vec<_>>();
if !aborted_versions.is_empty() {
Some(Object::new(
object.bucket_id,
object.key.clone(),
aborted_versions,
))
} else {
None
}
})
.collect::<Vec<_>>();
ret += abortions.len();
self.0.object_table.insert_many(abortions).await?;
if objects.len() < 1000 {
break;
} else {
start = Some(objects.last().unwrap().key.clone());
}
}
Ok(ret)
}
}
+6 -7
View File
@@ -30,12 +30,13 @@ rmp-serde = "0.15"
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
serde_bytes = "0.11"
serde_json = "1.0"
err-derive = { version = "0.3", optional = true }
# newer version requires rust edition 2021
kube = { version = "0.62", features = ["runtime", "derive"], optional = true }
k8s-openapi = { version = "0.13", features = ["v1_22"], optional = true }
openssl = { version = "0.10", features = ["vendored"], optional = true }
kube = { version = "0.75", default-features = false, features = ["runtime", "derive", "client", "rustls-tls"], optional = true }
k8s-openapi = { version = "0.16", features = ["v1_22"], optional = true }
schemars = { version = "0.8", optional = true }
reqwest = { version = "0.11", optional = true, default-features = false, features = ["rustls-tls-manual-roots", "json"] }
# newer version requires rust edition 2021
pnet_datalink = "0.28"
@@ -48,9 +49,7 @@ opentelemetry = "0.17"
netapp = { version = "0.5.2", features = ["telemetry"] }
hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] }
[features]
kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ]
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
consul-discovery = [ "reqwest", "err-derive" ]
system-libs = [ "sodiumoxide/use-pkg-config" ]
+126 -98
View File
@@ -1,16 +1,14 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::net::{IpAddr, SocketAddr};
use hyper::client::Client;
use hyper::StatusCode;
use hyper::{Body, Method, Request};
use err_derive::Error;
use serde::{Deserialize, Serialize};
use netapp::NodeID;
use garage_util::error::Error;
// ---- READING FROM CONSUL CATALOG ----
use garage_util::config::ConsulDiscoveryConfig;
#[derive(Deserialize, Clone, Debug)]
struct ConsulQueryEntry {
@@ -22,53 +20,6 @@ struct ConsulQueryEntry {
node_meta: HashMap<String, String>,
}
pub async fn get_consul_nodes(
consul_host: &str,
consul_service_name: &str,
) -> Result<Vec<(NodeID, SocketAddr)>, Error> {
let url = format!(
"http://{}/v1/catalog/service/{}",
consul_host, consul_service_name
);
let req = Request::builder()
.uri(url)
.method(Method::GET)
.body(Body::default())?;
let client = Client::new();
let resp = client.request(req).await?;
if resp.status() != StatusCode::OK {
return Err(Error::Message(format!("HTTP error {}", resp.status())));
}
let body = hyper::body::to_bytes(resp.into_body()).await?;
let entries = serde_json::from_slice::<Vec<ConsulQueryEntry>>(body.as_ref())?;
let mut ret = vec![];
for ent in entries {
let ip = ent.address.parse::<IpAddr>().ok();
let pubkey = ent
.node_meta
.get("pubkey")
.and_then(|k| hex::decode(&k).ok())
.and_then(|k| NodeID::from_slice(&k[..]));
if let (Some(ip), Some(pubkey)) = (ip, pubkey) {
ret.push((pubkey, SocketAddr::new(ip, ent.service_port)));
} else {
warn!(
"Could not process node spec from Consul: {:?} (invalid IP or public key)",
ent
);
}
}
debug!("Got nodes from Consul: {:?}", ret);
Ok(ret)
}
// ---- PUBLISHING TO CONSUL CATALOG ----
#[derive(Serialize, Clone, Debug)]
struct ConsulPublishEntry {
#[serde(rename = "Node")]
@@ -95,57 +46,134 @@ struct ConsulPublishService {
port: u16,
}
pub async fn publish_consul_service(
consul_host: &str,
consul_service_name: &str,
node_id: NodeID,
hostname: &str,
rpc_public_addr: SocketAddr,
) -> Result<(), Error> {
let node = format!("garage:{}", hex::encode(&node_id[..8]));
// ----
let advertisment = ConsulPublishEntry {
node: node.clone(),
address: rpc_public_addr.ip(),
node_meta: [
("pubkey".to_string(), hex::encode(node_id)),
("hostname".to_string(), hostname.to_string()),
]
.iter()
.cloned()
.collect(),
service: ConsulPublishService {
service_id: node.clone(),
service_name: consul_service_name.to_string(),
tags: vec!["advertised-by-garage".into(), hostname.into()],
address: rpc_public_addr.ip(),
port: rpc_public_addr.port(),
},
};
pub struct ConsulDiscovery {
config: ConsulDiscoveryConfig,
client: reqwest::Client,
}
let url = format!("http://{}/v1/catalog/register", consul_host);
let req_body = serde_json::to_string(&advertisment)?;
debug!("Request body for consul adv: {}", req_body);
impl ConsulDiscovery {
pub fn new(config: ConsulDiscoveryConfig) -> Result<Self, ConsulError> {
let client = match (&config.client_cert, &config.client_key) {
(Some(client_cert), Some(client_key)) => {
let mut client_cert_buf = vec![];
File::open(client_cert)?.read_to_end(&mut client_cert_buf)?;
let req = Request::builder()
.uri(url)
.method(Method::PUT)
.body(Body::from(req_body))?;
let mut client_key_buf = vec![];
File::open(client_key)?.read_to_end(&mut client_key_buf)?;
let client = Client::new();
let identity = reqwest::Identity::from_pem(
&[&client_cert_buf[..], &client_key_buf[..]].concat()[..],
)?;
let resp = client.request(req).await?;
debug!("Response of advertising to Consul: {:?}", resp);
let resp_code = resp.status();
let resp_bytes = &hyper::body::to_bytes(resp.into_body()).await?;
debug!(
"{}",
std::str::from_utf8(resp_bytes).unwrap_or("<invalid utf8>")
);
if config.tls_skip_verify {
reqwest::Client::builder()
.use_rustls_tls()
.danger_accept_invalid_certs(true)
.identity(identity)
.build()?
} else if let Some(ca_cert) = &config.ca_cert {
let mut ca_cert_buf = vec![];
File::open(ca_cert)?.read_to_end(&mut ca_cert_buf)?;
if resp_code != StatusCode::OK {
return Err(Error::Message(format!("HTTP error {}", resp_code)));
reqwest::Client::builder()
.use_rustls_tls()
.add_root_certificate(reqwest::Certificate::from_pem(&ca_cert_buf[..])?)
.identity(identity)
.build()?
} else {
reqwest::Client::builder()
.use_rustls_tls()
.identity(identity)
.build()?
}
}
(None, None) => reqwest::Client::new(),
_ => return Err(ConsulError::InvalidTLSConfig),
};
Ok(Self { client, config })
}
Ok(())
// ---- READING FROM CONSUL CATALOG ----
pub async fn get_consul_nodes(&self) -> Result<Vec<(NodeID, SocketAddr)>, ConsulError> {
let url = format!(
"{}/v1/catalog/service/{}",
self.config.consul_http_addr, self.config.service_name
);
let http = self.client.get(&url).send().await?;
let entries: Vec<ConsulQueryEntry> = http.json().await?;
let mut ret = vec![];
for ent in entries {
let ip = ent.address.parse::<IpAddr>().ok();
let pubkey = ent
.node_meta
.get("pubkey")
.and_then(|k| hex::decode(&k).ok())
.and_then(|k| NodeID::from_slice(&k[..]));
if let (Some(ip), Some(pubkey)) = (ip, pubkey) {
ret.push((pubkey, SocketAddr::new(ip, ent.service_port)));
} else {
warn!(
"Could not process node spec from Consul: {:?} (invalid IP or public key)",
ent
);
}
}
debug!("Got nodes from Consul: {:?}", ret);
Ok(ret)
}
// ---- PUBLISHING TO CONSUL CATALOG ----
pub async fn publish_consul_service(
&self,
node_id: NodeID,
hostname: &str,
rpc_public_addr: SocketAddr,
) -> Result<(), ConsulError> {
let node = format!("garage:{}", hex::encode(&node_id[..8]));
let advertisement = ConsulPublishEntry {
node: node.clone(),
address: rpc_public_addr.ip(),
node_meta: [
("pubkey".to_string(), hex::encode(node_id)),
("hostname".to_string(), hostname.to_string()),
]
.iter()
.cloned()
.collect(),
service: ConsulPublishService {
service_id: node.clone(),
service_name: self.config.service_name.clone(),
tags: vec!["advertised-by-garage".into(), hostname.into()],
address: rpc_public_addr.ip(),
port: rpc_public_addr.port(),
},
};
let url = format!("{}/v1/catalog/register", self.config.consul_http_addr);
let http = self.client.put(&url).json(&advertisement).send().await?;
http.error_for_status()?;
Ok(())
}
}
/// Regroup all Consul discovery errors
#[derive(Debug, Error)]
pub enum ConsulError {
#[error(display = "IO error: {}", _0)]
Io(#[error(source)] std::io::Error),
#[error(display = "HTTP error: {}", _0)]
Reqwest(#[error(source)] reqwest::Error),
#[error(display = "Invalid Consul TLS configuration")]
InvalidTLSConfig,
}
+8 -8
View File
@@ -12,6 +12,8 @@ use serde::{Deserialize, Serialize};
use netapp::NodeID;
use garage_util::config::KubernetesDiscoveryConfig;
static K8S_GROUP: &str = "deuxfleurs.fr";
#[derive(CustomResource, Debug, Serialize, Deserialize, Clone, JsonSchema)]
@@ -41,15 +43,14 @@ pub async fn create_kubernetes_crd() -> Result<(), kube::Error> {
}
pub async fn get_kubernetes_nodes(
kubernetes_service_name: &str,
kubernetes_namespace: &str,
kubernetes_config: &KubernetesDiscoveryConfig,
) -> Result<Vec<(NodeID, SocketAddr)>, kube::Error> {
let client = Client::try_default().await?;
let nodes: Api<GarageNode> = Api::namespaced(client.clone(), kubernetes_namespace);
let nodes: Api<GarageNode> = Api::namespaced(client.clone(), &kubernetes_config.namespace);
let lp = ListParams::default().labels(&format!(
"garage.{}/service={}",
K8S_GROUP, kubernetes_service_name
K8S_GROUP, kubernetes_config.service_name
));
let nodes = nodes.list(&lp).await?;
@@ -73,8 +74,7 @@ pub async fn get_kubernetes_nodes(
}
pub async fn publish_kubernetes_node(
kubernetes_service_name: &str,
kubernetes_namespace: &str,
kubernetes_config: &KubernetesDiscoveryConfig,
node_id: NodeID,
hostname: &str,
rpc_public_addr: SocketAddr,
@@ -93,13 +93,13 @@ pub async fn publish_kubernetes_node(
let labels = node.metadata.labels.insert(BTreeMap::new());
labels.insert(
format!("garage.{}/service", K8S_GROUP),
kubernetes_service_name.to_string(),
kubernetes_config.service_name.to_string(),
);
debug!("Node object to be applied: {:#?}", node);
let client = Client::try_default().await?;
let nodes: Api<GarageNode> = Api::namespaced(client.clone(), kubernetes_namespace);
let nodes: Api<GarageNode> = Api::namespaced(client.clone(), &kubernetes_config.namespace);
if let Ok(old_node) = nodes.get(&node_pubkey).await {
node.metadata.resource_version = old_node.metadata.resource_version;
+1
View File
@@ -3,6 +3,7 @@
#[macro_use]
extern crate tracing;
#[cfg(feature = "consul-discovery")]
mod consul;
#[cfg(feature = "kubernetes-discovery")]
mod kubernetes;
+30 -43
View File
@@ -23,12 +23,15 @@ use netapp::{NetApp, NetworkKey, NodeID, NodeKey};
use garage_util::background::BackgroundRunner;
use garage_util::config::Config;
#[cfg(feature = "kubernetes-discovery")]
use garage_util::config::KubernetesDiscoveryConfig;
use garage_util::data::*;
use garage_util::error::*;
use garage_util::persister::Persister;
use garage_util::time::*;
use crate::consul::*;
#[cfg(feature = "consul-discovery")]
use crate::consul::ConsulDiscovery;
#[cfg(feature = "kubernetes-discovery")]
use crate::kubernetes::*;
use crate::layout::*;
@@ -90,12 +93,14 @@ pub struct System {
system_endpoint: Arc<Endpoint<SystemRpc, System>>,
rpc_listen_addr: SocketAddr,
#[cfg(any(feature = "consul-discovery", feature = "kubernetes-discovery"))]
rpc_public_addr: Option<SocketAddr>,
bootstrap_peers: Vec<String>,
consul_discovery: Option<ConsulDiscoveryParam>,
#[cfg(feature = "consul-discovery")]
consul_discovery: Option<ConsulDiscovery>,
#[cfg(feature = "kubernetes-discovery")]
kubernetes_discovery: Option<KubernetesDiscoveryParam>,
kubernetes_discovery: Option<KubernetesDiscoveryConfig>,
replication_factor: usize,
@@ -285,29 +290,21 @@ impl System {
let system_endpoint = netapp.endpoint(SYSTEM_RPC_PATH.into());
let consul_discovery = match (&config.consul_host, &config.consul_service_name) {
(Some(ch), Some(csn)) => Some(ConsulDiscoveryParam {
consul_host: ch.to_string(),
service_name: csn.to_string(),
}),
_ => None,
};
#[cfg(feature = "kubernetes-discovery")]
let kubernetes_discovery = match (
&config.kubernetes_service_name,
&config.kubernetes_namespace,
) {
(Some(ksn), Some(kn)) => Some(KubernetesDiscoveryParam {
service_name: ksn.to_string(),
namespace: kn.to_string(),
skip_crd: config.kubernetes_skip_crd,
}),
_ => None,
#[cfg(feature = "consul-discovery")]
let consul_discovery = match &config.consul_discovery {
Some(cfg) => Some(
ConsulDiscovery::new(cfg.clone())
.ok_or_message("Invalid Consul discovery configuration")?,
),
None => None,
};
#[cfg(not(feature = "consul-discovery"))]
if config.consul_discovery.is_some() {
warn!("Consul discovery is not enabled in this build.");
}
#[cfg(not(feature = "kubernetes-discovery"))]
if config.kubernetes_service_name.is_some() || config.kubernetes_namespace.is_some() {
if config.kubernetes_discovery.is_some() {
warn!("Kubernetes discovery is not enabled in this build.");
}
@@ -329,11 +326,13 @@ impl System {
system_endpoint,
replication_factor,
rpc_listen_addr: config.rpc_bind_addr,
#[cfg(any(feature = "consul-discovery", feature = "kubernetes-discovery"))]
rpc_public_addr,
bootstrap_peers: config.bootstrap_peers.clone(),
#[cfg(feature = "consul-discovery")]
consul_discovery,
#[cfg(feature = "kubernetes-discovery")]
kubernetes_discovery,
kubernetes_discovery: config.kubernetes_discovery.clone(),
ring,
update_ring: Mutex::new(update_ring),
@@ -432,6 +431,7 @@ impl System {
// ---- INTERNALS ----
#[cfg(feature = "consul-discovery")]
async fn advertise_to_consul(self: Arc<Self>) -> Result<(), Error> {
let c = match &self.consul_discovery {
Some(c) => c,
@@ -446,9 +446,7 @@ impl System {
}
};
publish_consul_service(
&c.consul_host,
&c.service_name,
c.publish_consul_service(
self.netapp.id,
&self.local_status.load_full().hostname,
rpc_public_addr,
@@ -473,8 +471,7 @@ impl System {
};
publish_kubernetes_node(
&k.service_name,
&k.namespace,
k,
self.netapp.id,
&self.local_status.load_full().hostname,
rpc_public_addr,
@@ -644,8 +641,9 @@ impl System {
}
// Fetch peer list from Consul
#[cfg(feature = "consul-discovery")]
if let Some(c) = &self.consul_discovery {
match get_consul_nodes(&c.consul_host, &c.service_name).await {
match c.get_consul_nodes().await {
Ok(node_list) => {
ping_list.extend(node_list);
}
@@ -667,7 +665,7 @@ impl System {
};
}
match get_kubernetes_nodes(&k.service_name, &k.namespace).await {
match get_kubernetes_nodes(k).await {
Ok(node_list) => {
ping_list.extend(node_list);
}
@@ -691,6 +689,7 @@ impl System {
warn!("Could not save peer list to file: {}", e);
}
#[cfg(feature = "consul-discovery")]
self.background.spawn(self.clone().advertise_to_consul());
#[cfg(feature = "kubernetes-discovery")]
@@ -785,15 +784,3 @@ async fn resolve_peers(peers: &[String]) -> Vec<(NodeID, SocketAddr)> {
ret
}
struct ConsulDiscoveryParam {
consul_host: String,
service_name: String,
}
#[cfg(feature = "kubernetes-discovery")]
struct KubernetesDiscoveryParam {
service_name: String,
namespace: String,
skip_crd: bool,
}
+35 -10
View File
@@ -46,20 +46,17 @@ pub struct Config {
/// Timeout for Netapp RPC calls
pub rpc_timeout_msec: Option<u64>,
// -- Bootstraping and discovery
/// Bootstrap peers RPC address
#[serde(default)]
pub bootstrap_peers: Vec<String>,
/// Consul host to connect to to discover more peers
pub consul_host: Option<String>,
/// Consul service name to use
pub consul_service_name: Option<String>,
/// Kubernetes namespace the service discovery resources are be created in
pub kubernetes_namespace: Option<String>,
/// Service name to filter for in k8s custom resources
pub kubernetes_service_name: Option<String>,
/// Skip creation of the garagenodes CRD
/// Configuration for automatic node discovery through Consul
#[serde(default)]
pub kubernetes_skip_crd: bool,
pub consul_discovery: Option<ConsulDiscoveryConfig>,
/// Configuration for automatic node discovery through Kubernetes
#[serde(default)]
pub kubernetes_discovery: Option<KubernetesDiscoveryConfig>,
// -- DB
/// Database engine to use for metadata (options: sled, sqlite, lmdb)
@@ -129,6 +126,34 @@ pub struct AdminConfig {
pub trace_sink: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ConsulDiscoveryConfig {
/// Consul http or https address to connect to to discover more peers
pub consul_http_addr: String,
/// Consul service name to use
pub service_name: String,
/// CA TLS certificate to use when connecting to Consul
pub ca_cert: Option<String>,
/// Client TLS certificate to use when connecting to Consul
pub client_cert: Option<String>,
/// Client TLS key to use when connecting to Consul
pub client_key: Option<String>,
/// Skip TLS hostname verification
#[serde(default)]
pub tls_skip_verify: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct KubernetesDiscoveryConfig {
/// Kubernetes namespace the service discovery resources are be created in
pub namespace: String,
/// Service name to filter for in k8s custom resources
pub service_name: String,
/// Skip creation of the garagenodes CRD
#[serde(default)]
pub skip_crd: bool,
}
fn default_db_engine() -> String {
"sled".into()
}
+1 -1
View File
@@ -318,7 +318,7 @@ fn path_to_key<'a>(path: &'a str, index: &str) -> Result<Cow<'a, str>, Error> {
}
Some(_) => match path_utf8 {
Cow::Borrowed(pu8) => Ok((&pu8[1..]).into()),
Cow::Owned(pu8) => Ok((&pu8[1..]).to_string().into()),
Cow::Owned(pu8) => Ok(pu8[1..].to_string().into()),
},
}
}