WIP: Automatically create node layout, keys and buckets #883

Draft
apapsch wants to merge 6 commits from apapsch/garage:feature/auto-config into main
4 changed files with 186 additions and 1 deletions
Showing only changes of commit 89d70e6bd8 - Show all commits

View file

@ -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<SystemRpc, ()>,
admin_rpc_endpoint: &Endpoint<AdminRpc, ()>,
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<AdminRpc, ()>,
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<AdminRpc, ()>,
rpc_host: NodeID,
key_pattern: String,
) -> Result<bool, Error> {
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<AdminRpc, ()>,
rpc_host: NodeID,
name: String,
) -> Result<bool, Error> {
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<AdminRpc, ()>,
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<AdminRpc, ()>,
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<AdminRpc, ()>,
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))
}
}

View file

@ -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)]

View file

@ -284,7 +284,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
let system_rpc_endpoint = netapp.endpoint::<SystemRpc, ()>(SYSTEM_RPC_PATH.into());
let admin_rpc_endpoint = netapp.endpoint::<AdminRpc, ()>(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))),

View file

@ -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<AutoConfig>,
}
/// 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<String>,
}
/// Configuration to apply automatically
#[derive(Deserialize, Debug, Clone, Default)]
pub struct AutoConfig {
pub buckets: Vec<AutoBucket>,
/// Keys to automatically create on startup
pub keys: Vec<AutoKey>,
}
/// 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<AutoPermission>,
}
/// 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 {