From 89d70e6bd867bfa950fdc8b7d5acdd9c9315acfa Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Tue, 24 Sep 2024 14:09:13 +0200 Subject: [PATCH 1/6] Create keys, buckets and permissions. --- src/garage/cli/cmd.rs | 139 ++++++++++++++++++++++++++++++++++++++ src/garage/cli/structs.rs | 3 + src/garage/main.rs | 2 +- src/util/config.rs | 43 ++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 44d3d96c..fd43959b 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::time::Duration; use format_table::format_table; +use garage_util::config::{AutoBucket, AutoConfig, AutoKey, AutoPermission, Config}; use garage_util::error::*; use garage_rpc::layout::*; @@ -18,6 +19,7 @@ pub async fn cli_command_dispatch( system_rpc_endpoint: &Endpoint, admin_rpc_endpoint: &Endpoint, rpc_host: NodeID, + config: &Config, ) -> Result<(), HelperError> { match cmd { Command::Status => Ok(cmd_status(system_rpc_endpoint, rpc_host).await?), @@ -44,6 +46,9 @@ pub async fn cli_command_dispatch( Command::Meta(mo) => { cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::MetaOperation(mo)).await } + Command::Auto => { + cmd_auto(admin_rpc_endpoint, rpc_host, config.auto.as_ref()).await + } _ => unreachable!(), } } @@ -264,6 +269,42 @@ pub async fn cmd_admin( Ok(()) } +pub async fn cmd_auto( + rpc_cli: &Endpoint, + rpc_host: NodeID, + config: Option<&AutoConfig>, +) -> Result<(), HelperError> { + match config { + Some(auto) => { + + // Import keys + for key in auto.keys.iter() { + let exists = key_exists(rpc_cli, rpc_host, key.id.clone()).await?; + if !exists { + key_create(rpc_cli, rpc_host, key).await?; + } + } + + // Import buckets + for bucket in auto.buckets.iter() { + let exists = bucket_exists(rpc_cli, rpc_host, bucket.name.clone()).await?; + if !exists { + bucket_create(rpc_cli, rpc_host, bucket).await?; + } + + // Assign permissions to keys. + for perm in bucket.allow.iter() { + grant_permission(rpc_cli, rpc_host, bucket.name.clone(), perm).await?; + } + } + } + _ => { + println!("Auto configuration is missing"); + } + } + Ok(()) +} + // ---- utility ---- pub async fn fetch_status( @@ -278,3 +319,101 @@ pub async fn fetch_status( resp => Err(Error::unexpected_rpc_message(resp)), } } + +pub async fn key_exists( + rpc_cli: &Endpoint, + rpc_host: NodeID, + key_pattern: String, +) -> Result { + match rpc_cli + .call(&rpc_host, AdminRpc::KeyOperation( + KeyOperation::Info(KeyInfoOpt{ + key_pattern, + show_secret: false, + })), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } +} + +pub async fn bucket_exists( + rpc_cli: &Endpoint, + rpc_host: NodeID, + name: String, +) -> Result { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Info(BucketOpt{name}) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } +} + +pub async fn key_create( + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoKey, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::KeyOperation( + KeyOperation::Import(KeyImportOpt{ + name: params.name.clone(), + secret_key: params.secret.clone(), + key_id: params.id.clone(), + yes: true, + }) + ), PRIO_NORMAL).await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} + +pub async fn bucket_create( + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoBucket, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Create(BucketOpt{name: params.name.clone()}) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} + +pub async fn grant_permission( + rpc_cli: &Endpoint, + rpc_host: NodeID, + bucket_name: String, + perm: &AutoPermission, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Allow(PermBucketOpt{ + key_pattern: perm.key.clone(), + read: perm.read, + write: perm.write, + owner: perm.owner, + bucket: bucket_name, + }) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} \ No newline at end of file diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 6a9e6bfb..ebbe613d 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -59,6 +59,9 @@ pub enum Command { /// Convert metadata db between database engine formats #[structopt(name = "convert-db", version = garage_version())] ConvertDb(convert_db::ConvertDbOpt), + + /// Create preconfigured keys, buckets and node layout. + Auto, } #[derive(StructOpt, Debug)] diff --git a/src/garage/main.rs b/src/garage/main.rs index 92fd4d0c..44a6eae7 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -284,7 +284,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> { let system_rpc_endpoint = netapp.endpoint::(SYSTEM_RPC_PATH.into()); let admin_rpc_endpoint = netapp.endpoint::(ADMIN_RPC_PATH.into()); - match cli_command_dispatch(opt.cmd, &system_rpc_endpoint, &admin_rpc_endpoint, id).await { + match cli_command_dispatch(opt.cmd, &system_rpc_endpoint, &admin_rpc_endpoint, id, config.as_ref().unwrap()).await { Err(HelperError::Internal(i)) => Err(Error::Message(format!("Internal error: {}", i))), Err(HelperError::BadRequest(b)) => Err(Error::Message(b)), Err(e) => Err(Error::Message(format!("{}", e))), diff --git a/src/util/config.rs b/src/util/config.rs index 59329c0b..54cebe6f 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -128,6 +128,9 @@ pub struct Config { /// Configuration for the admin API endpoint #[serde(default = "Default::default")] pub admin: AdminConfig, + + /// Configuration to apply automatically + pub auto: Option, } /// Value for data_dir: either a single directory or a list of dirs with attributes @@ -198,6 +201,46 @@ pub struct AdminConfig { pub trace_sink: Option, } +/// Configuration to apply automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoConfig { + pub buckets: Vec, + + /// Keys to automatically create on startup + pub keys: Vec, +} + +/// Key to create automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoKey { + pub name: String, + pub id: String, + pub secret: String, +} + +/// Bucket to create automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoBucket { + pub name: String, + pub allow: Vec, +} + +/// Permission to create automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoPermission { + /// Key ID or name + pub key: String, + + /// Grant read permission + pub read: bool, + + /// Grant write permission + pub write: bool, + + /// Grant owner permission + pub owner: bool, +} + #[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum ConsulDiscoveryAPI { -- 2.45.2 From 8b659015d779d36a645f0e428c8efaef917cb45e Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Tue, 24 Sep 2024 15:52:11 +0200 Subject: [PATCH 2/6] Create node layout. --- src/garage/cli/auto.rs | 178 +++++++++++++++++++++++++++++++++++++++++ src/garage/cli/cmd.rs | 126 +++++------------------------ src/garage/cli/mod.rs | 2 + src/util/config.rs | 12 +++ 4 files changed, 210 insertions(+), 108 deletions(-) create mode 100644 src/garage/cli/auto.rs diff --git a/src/garage/cli/auto.rs b/src/garage/cli/auto.rs new file mode 100644 index 00000000..9c1519c3 --- /dev/null +++ b/src/garage/cli/auto.rs @@ -0,0 +1,178 @@ +use crate::admin::AdminRpc; +use crate::cli::{cmd_apply_layout, cmd_assign_role, fetch_layout, fetch_status, ApplyLayoutOpt, AssignRoleOpt, BucketOperation, BucketOpt, KeyImportOpt, KeyInfoOpt, KeyOperation, PermBucketOpt}; +use bytesize::ByteSize; +use garage_model::helper::error::Error as HelperError; +use garage_net::endpoint::Endpoint; +use garage_net::message::PRIO_NORMAL; +use garage_net::NodeID; +use garage_rpc::layout::NodeRoleV; +use garage_rpc::system::SystemRpc; +use garage_util::config::{AutoBucket, AutoKey, AutoNode, AutoPermission}; +use garage_util::data::Uuid; +use garage_util::error::Error; + +pub async fn key_exists( + rpc_cli: &Endpoint, + rpc_host: NodeID, + key_pattern: String, +) -> Result { + match rpc_cli + .call(&rpc_host, AdminRpc::KeyOperation( + KeyOperation::Info(KeyInfoOpt{ + key_pattern, + show_secret: false, + })), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } +} + +pub async fn bucket_exists( + rpc_cli: &Endpoint, + rpc_host: NodeID, + name: String, +) -> Result { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Info(BucketOpt{name}) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } +} + +pub async fn key_create( + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoKey, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::KeyOperation( + KeyOperation::Import(KeyImportOpt{ + name: params.name.clone(), + secret_key: params.secret.clone(), + key_id: params.id.clone(), + yes: true, + }) + ), PRIO_NORMAL).await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} + +pub async fn bucket_create( + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoBucket, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Create(BucketOpt{name: params.name.clone()}) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} + +pub async fn grant_permission( + rpc_cli: &Endpoint, + rpc_host: NodeID, + bucket_name: String, + perm: &AutoPermission, +) -> Result<(), Error> { + match rpc_cli + .call(&rpc_host, AdminRpc::BucketOperation( + BucketOperation::Allow(PermBucketOpt{ + key_pattern: perm.key.clone(), + read: perm.read, + write: perm.write, + owner: perm.owner, + bucket: bucket_name, + }) + ), PRIO_NORMAL) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)) + } +} + +pub async fn get_unassigned_nodes( + rpc_cli: &Endpoint, + rpc_host: NodeID, +) -> Result>, Error> { + let status = fetch_status(rpc_cli, rpc_host).await?; + let layout = fetch_layout(rpc_cli, rpc_host).await?; + let mut nodes: Vec = Vec::new(); + + for adv in status.iter().filter(|adv| adv.is_up) { + if layout.current().roles.get(&adv.id).is_none() { + let prev_role = layout + .versions + .iter() + .rev() + .find_map(|x| match x.roles.get(&adv.id) { + Some(NodeRoleV(Some(cfg))) => Some(cfg), + _ => None, + }); + if prev_role.is_none() { + if let Some(NodeRoleV(Some(_))) = layout.staging.get().roles.get(&adv.id) { + // Node role assignment is pending, can return immediately. + return Ok(None); + } else { + nodes.push(adv.id.clone()); + } + } + } else { + // Node role is assigned, can return immediately. + return Ok(None); + } + } + + // Encountered no node with an assignment (pending or applied). + // Therefore, all nodes are unassigned. + Ok(Some(nodes)) +} + +pub async fn assign_node_layout( + rpc_cli: &Endpoint, + rpc_host: NodeID, + unassigned_nodes: &Vec, + auto_nodes: &Vec, +) -> Result<(), Error> { + if unassigned_nodes.len() != auto_nodes.len() { + return Err(Error::Message("Cannot apply auto layout: configured nodes do not match actual nodes".to_string())); + } + + for (i, node_id) in unassigned_nodes.iter().enumerate() { + if let Some(auto) = auto_nodes.get(i) { + let capacity = auto.capacity.parse::()?; + cmd_assign_role(rpc_cli, rpc_host, AssignRoleOpt{ + node_ids: vec![format!("{id:?}", id=node_id)], + zone: Some(auto.zone.clone()), + capacity: Some(capacity), + gateway: false, + tags: vec![], + replace: vec![], + }).await?; + } + } + + cmd_apply_layout(rpc_cli, rpc_host, ApplyLayoutOpt{ + version: Some(1), + }).await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index fd43959b..a072937d 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::time::Duration; use format_table::format_table; -use garage_util::config::{AutoBucket, AutoConfig, AutoKey, AutoPermission, Config}; +use garage_util::config::{AutoConfig, Config}; use garage_util::error::*; use garage_rpc::layout::*; @@ -47,7 +47,7 @@ pub async fn cli_command_dispatch( cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::MetaOperation(mo)).await } Command::Auto => { - cmd_auto(admin_rpc_endpoint, rpc_host, config.auto.as_ref()).await + cmd_auto(admin_rpc_endpoint, system_rpc_endpoint, rpc_host, config.auto.as_ref()).await } _ => unreachable!(), } @@ -270,36 +270,44 @@ pub async fn cmd_admin( } pub async fn cmd_auto( - rpc_cli: &Endpoint, + rpc_admin: &Endpoint, + rpc_system: &Endpoint, rpc_host: NodeID, - config: Option<&AutoConfig>, + config: Option<&AutoConfig>, ) -> Result<(), HelperError> { match config { Some(auto) => { + // Assign cluster layout if all nodes are unassigned. + // This is to ensure a newly created cluster is readily available. + // Further changes to the cluster layout must be done manually. + if let Some(nodes) = get_unassigned_nodes(rpc_system, rpc_host).await? { + assign_node_layout(rpc_system, rpc_host, &nodes, auto.nodes.as_ref()).await?; + } + // Import keys for key in auto.keys.iter() { - let exists = key_exists(rpc_cli, rpc_host, key.id.clone()).await?; + let exists = key_exists(rpc_admin, rpc_host, key.id.clone()).await?; if !exists { - key_create(rpc_cli, rpc_host, key).await?; + key_create(rpc_admin, rpc_host, key).await?; } } // Import buckets for bucket in auto.buckets.iter() { - let exists = bucket_exists(rpc_cli, rpc_host, bucket.name.clone()).await?; + let exists = bucket_exists(rpc_admin, rpc_host, bucket.name.clone()).await?; if !exists { - bucket_create(rpc_cli, rpc_host, bucket).await?; + bucket_create(rpc_admin, rpc_host, bucket).await?; } // Assign permissions to keys. for perm in bucket.allow.iter() { - grant_permission(rpc_cli, rpc_host, bucket.name.clone(), perm).await?; + grant_permission(rpc_admin, rpc_host, bucket.name.clone(), perm).await?; } } } _ => { - println!("Auto configuration is missing"); + return Err(HelperError::BadRequest("Auto configuration is missing".to_string())) } } Ok(()) @@ -318,102 +326,4 @@ pub async fn fetch_status( SystemRpc::ReturnKnownNodes(nodes) => Ok(nodes), resp => Err(Error::unexpected_rpc_message(resp)), } -} - -pub async fn key_exists( - rpc_cli: &Endpoint, - rpc_host: NodeID, - key_pattern: String, -) -> Result { - match rpc_cli - .call(&rpc_host, AdminRpc::KeyOperation( - KeyOperation::Info(KeyInfoOpt{ - key_pattern, - show_secret: false, - })), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(true), - Err(HelperError::BadRequest(_)) => Ok(false), - resp => Err(Error::unexpected_rpc_message(resp)), - } -} - -pub async fn bucket_exists( - rpc_cli: &Endpoint, - rpc_host: NodeID, - name: String, -) -> Result { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Info(BucketOpt{name}) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(true), - Err(HelperError::BadRequest(_)) => Ok(false), - resp => Err(Error::unexpected_rpc_message(resp)), - } -} - -pub async fn key_create( - rpc_cli: &Endpoint, - rpc_host: NodeID, - params: &AutoKey, -) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::KeyOperation( - KeyOperation::Import(KeyImportOpt{ - name: params.name.clone(), - secret_key: params.secret.clone(), - key_id: params.id.clone(), - yes: true, - }) - ), PRIO_NORMAL).await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } -} - -pub async fn bucket_create( - rpc_cli: &Endpoint, - rpc_host: NodeID, - params: &AutoBucket, -) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Create(BucketOpt{name: params.name.clone()}) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } -} - -pub async fn grant_permission( - rpc_cli: &Endpoint, - rpc_host: NodeID, - bucket_name: String, - perm: &AutoPermission, -) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Allow(PermBucketOpt{ - key_pattern: perm.key.clone(), - read: perm.read, - write: perm.write, - owner: perm.owner, - bucket: bucket_name, - }) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } } \ No newline at end of file diff --git a/src/garage/cli/mod.rs b/src/garage/cli/mod.rs index e131f62c..699be6ff 100644 --- a/src/garage/cli/mod.rs +++ b/src/garage/cli/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod init; pub(crate) mod layout; pub(crate) mod structs; pub(crate) mod util; +pub(crate) mod auto; pub(crate) mod convert_db; @@ -11,3 +12,4 @@ pub(crate) use init::*; pub(crate) use layout::*; pub(crate) use structs::*; pub(crate) use util::*; +pub(crate) use auto::*; \ No newline at end of file diff --git a/src/util/config.rs b/src/util/config.rs index 54cebe6f..75711a01 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -208,6 +208,9 @@ pub struct AutoConfig { /// Keys to automatically create on startup pub keys: Vec, + + /// Node layout to automatically configure. + pub nodes: Vec, } /// Key to create automatically @@ -241,6 +244,15 @@ pub struct AutoPermission { pub owner: bool, } +/// Node layout to create automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoNode { + /// Zone name + pub zone: String, + /// Storage capacity, in bytes (or with suffix) + pub capacity: String, +} + #[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum ConsulDiscoveryAPI { -- 2.45.2 From 53a8d8e720d51b32607528a2f4ecf05b66781b9d Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Tue, 24 Sep 2024 16:05:33 +0200 Subject: [PATCH 3/6] Improve comments. --- src/util/config.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/util/config.rs b/src/util/config.rs index 75711a01..79897077 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -201,30 +201,36 @@ pub struct AdminConfig { pub trace_sink: Option, } -/// Configuration to apply automatically +/// Configuration to apply without manual intervention #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoConfig { + /// Buckets to create automatically pub buckets: Vec, - /// Keys to automatically create on startup + /// Keys to create automatically pub keys: Vec, - /// Node layout to automatically configure. + /// Node layout to create automatically pub nodes: Vec, } /// Key to create automatically #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoKey { + /// Key name pub name: String, + /// Key ID starting with GK pub id: String, + /// Secret key pub secret: String, } /// Bucket to create automatically #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoBucket { + /// Bucket name pub name: String, + /// Permissions to grant on bucket to given keys pub allow: Vec, } -- 2.45.2 From b185ec2f85c124222e132c0c281b25b2f4829214 Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Tue, 24 Sep 2024 16:34:26 +0200 Subject: [PATCH 4/6] Run cargo fmt. --- src/block/layout.rs | 3 +- src/garage/cli/auto.rs | 292 ++++++++++++++++++++----------------- src/garage/cli/cmd.rs | 47 +++--- src/garage/cli/mod.rs | 4 +- src/garage/cli/structs.rs | 4 +- src/garage/main.rs | 10 +- src/model/helper/locked.rs | 3 +- src/util/config.rs | 26 ++-- 8 files changed, 214 insertions(+), 175 deletions(-) diff --git a/src/block/layout.rs b/src/block/layout.rs index e78f3f08..00e3debb 100644 --- a/src/block/layout.rs +++ b/src/block/layout.rs @@ -279,7 +279,8 @@ impl DataLayout { u16::from_be_bytes([ hash.as_slice()[HASH_DRIVE_BYTES.0], hash.as_slice()[HASH_DRIVE_BYTES.1], - ]) as usize % DRIVE_NPART + ]) as usize + % DRIVE_NPART } fn block_dir_from(&self, hash: &Hash, dir: &PathBuf) -> PathBuf { diff --git a/src/garage/cli/auto.rs b/src/garage/cli/auto.rs index 9c1519c3..a42e286a 100644 --- a/src/garage/cli/auto.rs +++ b/src/garage/cli/auto.rs @@ -1,5 +1,8 @@ use crate::admin::AdminRpc; -use crate::cli::{cmd_apply_layout, cmd_assign_role, fetch_layout, fetch_status, ApplyLayoutOpt, AssignRoleOpt, BucketOperation, BucketOpt, KeyImportOpt, KeyInfoOpt, KeyOperation, PermBucketOpt}; +use crate::cli::{ + cmd_apply_layout, cmd_assign_role, fetch_layout, fetch_status, ApplyLayoutOpt, AssignRoleOpt, + BucketOperation, BucketOpt, KeyImportOpt, KeyInfoOpt, KeyOperation, PermBucketOpt, +}; use bytesize::ByteSize; use garage_model::helper::error::Error as HelperError; use garage_net::endpoint::Endpoint; @@ -12,167 +15,186 @@ use garage_util::data::Uuid; use garage_util::error::Error; pub async fn key_exists( - rpc_cli: &Endpoint, - rpc_host: NodeID, - key_pattern: String, + rpc_cli: &Endpoint, + rpc_host: NodeID, + key_pattern: String, ) -> Result { - match rpc_cli - .call(&rpc_host, AdminRpc::KeyOperation( - KeyOperation::Info(KeyInfoOpt{ - key_pattern, - show_secret: false, - })), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(true), - Err(HelperError::BadRequest(_)) => Ok(false), - resp => Err(Error::unexpected_rpc_message(resp)), - } + match rpc_cli + .call( + &rpc_host, + AdminRpc::KeyOperation(KeyOperation::Info(KeyInfoOpt { + key_pattern, + show_secret: false, + })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } } pub async fn bucket_exists( - rpc_cli: &Endpoint, - rpc_host: NodeID, - name: String, + rpc_cli: &Endpoint, + rpc_host: NodeID, + name: String, ) -> Result { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Info(BucketOpt{name}) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(true), - Err(HelperError::BadRequest(_)) => Ok(false), - resp => Err(Error::unexpected_rpc_message(resp)), - } + match rpc_cli + .call( + &rpc_host, + AdminRpc::BucketOperation(BucketOperation::Info(BucketOpt { name })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(true), + Err(HelperError::BadRequest(_)) => Ok(false), + resp => Err(Error::unexpected_rpc_message(resp)), + } } pub async fn key_create( - rpc_cli: &Endpoint, - rpc_host: NodeID, - params: &AutoKey, + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoKey, ) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::KeyOperation( - KeyOperation::Import(KeyImportOpt{ - name: params.name.clone(), - secret_key: params.secret.clone(), - key_id: params.id.clone(), - yes: true, - }) - ), PRIO_NORMAL).await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } + match rpc_cli + .call( + &rpc_host, + AdminRpc::KeyOperation(KeyOperation::Import(KeyImportOpt { + name: params.name.clone(), + secret_key: params.secret.clone(), + key_id: params.id.clone(), + yes: true, + })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)), + } } pub async fn bucket_create( - rpc_cli: &Endpoint, - rpc_host: NodeID, - params: &AutoBucket, + rpc_cli: &Endpoint, + rpc_host: NodeID, + params: &AutoBucket, ) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Create(BucketOpt{name: params.name.clone()}) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } + match rpc_cli + .call( + &rpc_host, + AdminRpc::BucketOperation(BucketOperation::Create(BucketOpt { + name: params.name.clone(), + })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)), + } } pub async fn grant_permission( - rpc_cli: &Endpoint, - rpc_host: NodeID, - bucket_name: String, - perm: &AutoPermission, + rpc_cli: &Endpoint, + rpc_host: NodeID, + bucket_name: String, + perm: &AutoPermission, ) -> Result<(), Error> { - match rpc_cli - .call(&rpc_host, AdminRpc::BucketOperation( - BucketOperation::Allow(PermBucketOpt{ - key_pattern: perm.key.clone(), - read: perm.read, - write: perm.write, - owner: perm.owner, - bucket: bucket_name, - }) - ), PRIO_NORMAL) - .await? - { - Ok(_) => Ok(()), - Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), - resp => Err(Error::unexpected_rpc_message(resp)) - } + match rpc_cli + .call( + &rpc_host, + AdminRpc::BucketOperation(BucketOperation::Allow(PermBucketOpt { + key_pattern: perm.key.clone(), + read: perm.read, + write: perm.write, + owner: perm.owner, + bucket: bucket_name, + })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)), + } } pub async fn get_unassigned_nodes( - rpc_cli: &Endpoint, - rpc_host: NodeID, + rpc_cli: &Endpoint, + rpc_host: NodeID, ) -> Result>, Error> { - let status = fetch_status(rpc_cli, rpc_host).await?; - let layout = fetch_layout(rpc_cli, rpc_host).await?; - let mut nodes: Vec = Vec::new(); + let status = fetch_status(rpc_cli, rpc_host).await?; + let layout = fetch_layout(rpc_cli, rpc_host).await?; + let mut nodes: Vec = Vec::new(); - for adv in status.iter().filter(|adv| adv.is_up) { - if layout.current().roles.get(&adv.id).is_none() { - let prev_role = layout - .versions - .iter() - .rev() - .find_map(|x| match x.roles.get(&adv.id) { - Some(NodeRoleV(Some(cfg))) => Some(cfg), - _ => None, - }); - if prev_role.is_none() { - if let Some(NodeRoleV(Some(_))) = layout.staging.get().roles.get(&adv.id) { - // Node role assignment is pending, can return immediately. - return Ok(None); - } else { - nodes.push(adv.id.clone()); - } - } - } else { - // Node role is assigned, can return immediately. - return Ok(None); - } - } + for adv in status.iter().filter(|adv| adv.is_up) { + if layout.current().roles.get(&adv.id).is_none() { + let prev_role = layout + .versions + .iter() + .rev() + .find_map(|x| match x.roles.get(&adv.id) { + Some(NodeRoleV(Some(cfg))) => Some(cfg), + _ => None, + }); + if prev_role.is_none() { + if let Some(NodeRoleV(Some(_))) = layout.staging.get().roles.get(&adv.id) { + // Node role assignment is pending, can return immediately. + return Ok(None); + } else { + nodes.push(adv.id.clone()); + } + } + } else { + // Node role is assigned, can return immediately. + return Ok(None); + } + } - // Encountered no node with an assignment (pending or applied). - // Therefore, all nodes are unassigned. - Ok(Some(nodes)) + // Encountered no node with an assignment (pending or applied). + // Therefore, all nodes are unassigned. + Ok(Some(nodes)) } pub async fn assign_node_layout( - rpc_cli: &Endpoint, - rpc_host: NodeID, - unassigned_nodes: &Vec, - auto_nodes: &Vec, + rpc_cli: &Endpoint, + rpc_host: NodeID, + unassigned_nodes: &Vec, + auto_nodes: &Vec, ) -> Result<(), Error> { - if unassigned_nodes.len() != auto_nodes.len() { - return Err(Error::Message("Cannot apply auto layout: configured nodes do not match actual nodes".to_string())); - } + if unassigned_nodes.len() != auto_nodes.len() { + return Err(Error::Message( + "Cannot apply auto layout: configured nodes do not match actual nodes".to_string(), + )); + } - for (i, node_id) in unassigned_nodes.iter().enumerate() { - if let Some(auto) = auto_nodes.get(i) { - let capacity = auto.capacity.parse::()?; - cmd_assign_role(rpc_cli, rpc_host, AssignRoleOpt{ - node_ids: vec![format!("{id:?}", id=node_id)], - zone: Some(auto.zone.clone()), - capacity: Some(capacity), - gateway: false, - tags: vec![], - replace: vec![], - }).await?; - } - } + for (i, node_id) in unassigned_nodes.iter().enumerate() { + if let Some(auto) = auto_nodes.get(i) { + let capacity = auto.capacity.parse::()?; + cmd_assign_role( + rpc_cli, + rpc_host, + AssignRoleOpt { + node_ids: vec![format!("{id:?}", id = node_id)], + zone: Some(auto.zone.clone()), + capacity: Some(capacity), + gateway: false, + tags: vec![], + replace: vec![], + }, + ) + .await?; + } + } - cmd_apply_layout(rpc_cli, rpc_host, ApplyLayoutOpt{ - version: Some(1), - }).await?; + cmd_apply_layout(rpc_cli, rpc_host, ApplyLayoutOpt { version: Some(1) }).await?; - Ok(()) -} \ No newline at end of file + Ok(()) +} diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index a072937d..642dd920 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -19,7 +19,7 @@ pub async fn cli_command_dispatch( system_rpc_endpoint: &Endpoint, admin_rpc_endpoint: &Endpoint, rpc_host: NodeID, - config: &Config, + config: &Config, ) -> Result<(), HelperError> { match cmd { Command::Status => Ok(cmd_status(system_rpc_endpoint, rpc_host).await?), @@ -46,9 +46,15 @@ pub async fn cli_command_dispatch( Command::Meta(mo) => { cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::MetaOperation(mo)).await } - Command::Auto => { - cmd_auto(admin_rpc_endpoint, system_rpc_endpoint, rpc_host, config.auto.as_ref()).await - } + Command::Auto => { + cmd_auto( + admin_rpc_endpoint, + system_rpc_endpoint, + rpc_host, + config.auto.as_ref(), + ) + .await + } _ => unreachable!(), } } @@ -275,9 +281,8 @@ pub async fn cmd_auto( rpc_host: NodeID, config: Option<&AutoConfig>, ) -> Result<(), HelperError> { - match config { - Some(auto) => { - + match config { + Some(auto) => { // Assign cluster layout if all nodes are unassigned. // This is to ensure a newly created cluster is readily available. // Further changes to the cluster layout must be done manually. @@ -286,12 +291,12 @@ pub async fn cmd_auto( } // Import keys - for key in auto.keys.iter() { - let exists = key_exists(rpc_admin, rpc_host, key.id.clone()).await?; - if !exists { - key_create(rpc_admin, rpc_host, key).await?; - } - } + for key in auto.keys.iter() { + let exists = key_exists(rpc_admin, rpc_host, key.id.clone()).await?; + if !exists { + key_create(rpc_admin, rpc_host, key).await?; + } + } // Import buckets for bucket in auto.buckets.iter() { @@ -305,12 +310,14 @@ pub async fn cmd_auto( grant_permission(rpc_admin, rpc_host, bucket.name.clone(), perm).await?; } } - } - _ => { - return Err(HelperError::BadRequest("Auto configuration is missing".to_string())) - } - } - Ok(()) + } + _ => { + return Err(HelperError::BadRequest( + "Auto configuration is missing".to_string(), + )) + } + } + Ok(()) } // ---- utility ---- @@ -326,4 +333,4 @@ pub async fn fetch_status( SystemRpc::ReturnKnownNodes(nodes) => Ok(nodes), resp => Err(Error::unexpected_rpc_message(resp)), } -} \ No newline at end of file +} diff --git a/src/garage/cli/mod.rs b/src/garage/cli/mod.rs index 699be6ff..5c27665e 100644 --- a/src/garage/cli/mod.rs +++ b/src/garage/cli/mod.rs @@ -1,15 +1,15 @@ +pub(crate) mod auto; pub(crate) mod cmd; pub(crate) mod init; pub(crate) mod layout; pub(crate) mod structs; pub(crate) mod util; -pub(crate) mod auto; pub(crate) mod convert_db; +pub(crate) use auto::*; pub(crate) use cmd::*; pub(crate) use init::*; pub(crate) use layout::*; pub(crate) use structs::*; pub(crate) use util::*; -pub(crate) use auto::*; \ No newline at end of file diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index ebbe613d..4197029c 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -60,8 +60,8 @@ pub enum Command { #[structopt(name = "convert-db", version = garage_version())] ConvertDb(convert_db::ConvertDbOpt), - /// Create preconfigured keys, buckets and node layout. - Auto, + /// Create preconfigured keys, buckets and node layout. + Auto, } #[derive(StructOpt, Debug)] diff --git a/src/garage/main.rs b/src/garage/main.rs index 44a6eae7..9c502140 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -284,7 +284,15 @@ async fn cli_command(opt: Opt) -> Result<(), Error> { let system_rpc_endpoint = netapp.endpoint::(SYSTEM_RPC_PATH.into()); let admin_rpc_endpoint = netapp.endpoint::(ADMIN_RPC_PATH.into()); - match cli_command_dispatch(opt.cmd, &system_rpc_endpoint, &admin_rpc_endpoint, id, config.as_ref().unwrap()).await { + match cli_command_dispatch( + opt.cmd, + &system_rpc_endpoint, + &admin_rpc_endpoint, + id, + config.as_ref().unwrap(), + ) + .await + { Err(HelperError::Internal(i)) => Err(Error::Message(format!("Internal error: {}", i))), Err(HelperError::BadRequest(b)) => Err(Error::Message(b)), Err(e) => Err(Error::Message(format!("{}", e))), diff --git a/src/model/helper/locked.rs b/src/model/helper/locked.rs index f8e06add..b541d548 100644 --- a/src/model/helper/locked.rs +++ b/src/model/helper/locked.rs @@ -279,7 +279,8 @@ impl<'a> LockedHelper<'a> { .local_aliases .get(alias_name) .cloned() - .flatten() != Some(bucket_id) + .flatten() + != Some(bucket_id) { return Err(GarageError::Message(format!( "Bucket {:?} does not have alias {} in namespace of key {}", diff --git a/src/util/config.rs b/src/util/config.rs index 79897077..70ea8bfb 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -129,8 +129,8 @@ pub struct Config { #[serde(default = "Default::default")] pub admin: AdminConfig, - /// Configuration to apply automatically - pub auto: Option, + /// Configuration to apply automatically + pub auto: Option, } /// Value for data_dir: either a single directory or a list of dirs with attributes @@ -205,10 +205,10 @@ pub struct AdminConfig { #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoConfig { /// Buckets to create automatically - pub buckets: Vec, - - /// Keys to create automatically - pub keys: Vec, + pub buckets: Vec, + + /// Keys to create automatically + pub keys: Vec, /// Node layout to create automatically pub nodes: Vec, @@ -218,27 +218,27 @@ pub struct AutoConfig { #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoKey { /// Key name - pub name: String, + pub name: String, /// Key ID starting with GK - pub id: String, + pub id: String, /// Secret key - pub secret: String, + pub secret: String, } /// Bucket to create automatically #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoBucket { /// Bucket name - pub name: String, + pub name: String, /// Permissions to grant on bucket to given keys - pub allow: Vec, + pub allow: Vec, } /// Permission to create automatically #[derive(Deserialize, Debug, Clone, Default)] pub struct AutoPermission { - /// Key ID or name - pub key: String, + /// Key ID or name + pub key: String, /// Grant read permission pub read: bool, -- 2.45.2 From c2a6108d29427f162a646396f865b58a664744bd Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Tue, 24 Sep 2024 16:47:30 +0200 Subject: [PATCH 5/6] Silence remaining cargo fmt complaints. --- src/block/layout.rs | 3 +-- src/model/helper/locked.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/block/layout.rs b/src/block/layout.rs index 00e3debb..e78f3f08 100644 --- a/src/block/layout.rs +++ b/src/block/layout.rs @@ -279,8 +279,7 @@ impl DataLayout { u16::from_be_bytes([ hash.as_slice()[HASH_DRIVE_BYTES.0], hash.as_slice()[HASH_DRIVE_BYTES.1], - ]) as usize - % DRIVE_NPART + ]) as usize % DRIVE_NPART } fn block_dir_from(&self, hash: &Hash, dir: &PathBuf) -> PathBuf { diff --git a/src/model/helper/locked.rs b/src/model/helper/locked.rs index b541d548..f8e06add 100644 --- a/src/model/helper/locked.rs +++ b/src/model/helper/locked.rs @@ -279,8 +279,7 @@ impl<'a> LockedHelper<'a> { .local_aliases .get(alias_name) .cloned() - .flatten() - != Some(bucket_id) + .flatten() != Some(bucket_id) { return Err(GarageError::Message(format!( "Bucket {:?} does not have alias {} in namespace of key {}", -- 2.45.2 From 7af19649bb1d5fcde81c9f3618c2501f47bf575c Mon Sep 17 00:00:00 2001 From: Aljosha Papsch Date: Thu, 26 Sep 2024 14:33:45 +0200 Subject: [PATCH 6/6] Configure website access. --- src/garage/cli/auto.rs | 33 ++++++++++++++++++++++++++++----- src/garage/cli/cmd.rs | 5 +++++ src/util/config.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/garage/cli/auto.rs b/src/garage/cli/auto.rs index a42e286a..2496dc60 100644 --- a/src/garage/cli/auto.rs +++ b/src/garage/cli/auto.rs @@ -1,8 +1,5 @@ use crate::admin::AdminRpc; -use crate::cli::{ - cmd_apply_layout, cmd_assign_role, fetch_layout, fetch_status, ApplyLayoutOpt, AssignRoleOpt, - BucketOperation, BucketOpt, KeyImportOpt, KeyInfoOpt, KeyOperation, PermBucketOpt, -}; +use crate::cli::{cmd_apply_layout, cmd_assign_role, fetch_layout, fetch_status, ApplyLayoutOpt, AssignRoleOpt, BucketOperation, BucketOpt, KeyImportOpt, KeyInfoOpt, KeyOperation, PermBucketOpt, WebsiteOpt}; use bytesize::ByteSize; use garage_model::helper::error::Error as HelperError; use garage_net::endpoint::Endpoint; @@ -10,7 +7,7 @@ use garage_net::message::PRIO_NORMAL; use garage_net::NodeID; use garage_rpc::layout::NodeRoleV; use garage_rpc::system::SystemRpc; -use garage_util::config::{AutoBucket, AutoKey, AutoNode, AutoPermission}; +use garage_util::config::{AutoBucket, AutoBucketWebsite, AutoKey, AutoNode, AutoPermission, WebsiteAllowance}; use garage_util::data::Uuid; use garage_util::error::Error; @@ -100,6 +97,32 @@ pub async fn bucket_create( } } +pub async fn bucket_configure_website( + rpc_cli: &Endpoint, + rpc_host: NodeID, + bucket_name: String, + website: &AutoBucketWebsite, +) -> Result<(), Error> { + match rpc_cli + .call( + &rpc_host, + AdminRpc::BucketOperation(BucketOperation::Website(WebsiteOpt{ + allow: matches!(website.mode, WebsiteAllowance::Allow), + deny: matches!(website.mode, WebsiteAllowance::Deny), + bucket: bucket_name.clone(), + index_document: website.index_document.clone(), + error_document: website.error_document.clone(), + })), + PRIO_NORMAL, + ) + .await? + { + Ok(_) => Ok(()), + Err(HelperError::BadRequest(msg)) => Err(Error::Message(msg)), + resp => Err(Error::unexpected_rpc_message(resp)), + } +} + pub async fn grant_permission( rpc_cli: &Endpoint, rpc_host: NodeID, diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 642dd920..1398eb20 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -309,6 +309,11 @@ pub async fn cmd_auto( for perm in bucket.allow.iter() { grant_permission(rpc_admin, rpc_host, bucket.name.clone(), perm).await?; } + + // Configure website access. + if let Some(website) = bucket.website.as_ref() { + bucket_configure_website(rpc_admin, rpc_host, bucket.name.clone(), website).await? + } } } _ => { diff --git a/src/util/config.rs b/src/util/config.rs index 70ea8bfb..37fd2175 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -232,6 +232,33 @@ pub struct AutoBucket { pub name: String, /// Permissions to grant on bucket to given keys pub allow: Vec, + /// Website configuration + pub website: Option +} + +fn default_index_document() -> String { + "index.html".to_string() +} + +/// Bucket website configuration to create automatically +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AutoBucketWebsite { + /// Allow or deny (default) website access + #[serde(default)] + pub mode: WebsiteAllowance, + /// Error document: the optional document returned when an error occurs + pub error_document: Option, + /// Index document: the suffix appended to request paths ending by / + #[serde(default = "default_index_document")] + pub index_document: String, +} + +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "lowercase")] +pub enum WebsiteAllowance { + Allow, + #[default] + Deny, } /// Permission to create automatically -- 2.45.2