diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 4bb6dc0a..9073991d 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -85,9 +85,18 @@ pub enum Command { #[derive(StructOpt, Debug)] pub struct ServerOpt { - /// Automatically configure a single-node layout in the cluster + /// Automatically configure a single-node layout in the cluster. + /// Garage will refuse to run if the cluster already has other nodes. #[structopt(long = "single-node")] pub(crate) single_node: bool, + /// Configure a default S3 API key using environment variables `GARAGE_DEFAULT_ACCESS_KEY` and + /// `GARAGE_DEFAULT_SECRET_KEY`. Requires `--single-node`. + #[structopt(long = "default-access-key")] + pub(crate) default_access_key: bool, + /// Configure a default bucket using environment variable `GARAGE_DEFAULT_BUCKET`. + /// Implies `--default-access-key`. Requires `--single-node`. + #[structopt(long = "default-bucket")] + pub(crate) default_bucket: bool, } // ------------------------- diff --git a/src/garage/server.rs b/src/garage/server.rs index 7f80b205..93c29562 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -50,6 +50,9 @@ pub async fn run_server( info!("Initializing Garage main data store..."); let garage = Garage::new(config.clone())?; + // Handle --single-node, --default-bucket and --default-access-key + initial_config(&garage, opt).await?; + info!("Initializing background runner..."); let watch_cancel = watch_shutdown_signal(); let (background, await_background_done) = BackgroundRunner::new(watch_cancel.clone()); @@ -73,10 +76,6 @@ pub async fn run_server( info!("Launching internal Garage cluster communications..."); let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone())); - // ---- Run initial configuration logic ---- - - initial_config(&garage, opt).await?; - // ---- Launch public-facing API servers ---- let mut servers = vec![]; @@ -175,9 +174,23 @@ pub async fn run_server( } async fn initial_config(garage: &Arc, opt: ServerOpt) -> Result<(), Error> { + use garage_model::bucket_alias_table::is_valid_bucket_name; + use garage_model::bucket_table::Bucket; + use garage_model::key_table::*; + use garage_model::permission::BucketKeyPerm; use garage_rpc::layout::*; + use garage_rpc::replication_mode::ReplicationFactor; + use garage_table::*; + use garage_util::time::now_msec; if opt.single_node { + if garage.replication_factor != ReplicationFactor::new(1).unwrap() { + return Err(Error::Message( + "Single-node mode requires replication_factor = 1 in the configuration file." + .into(), + )); + } + let layout_version = garage.system.cluster_layout().inner().current().version; if layout_version > 1 { @@ -229,6 +242,104 @@ async fn initial_config(garage: &Arc, opt: ServerOpt) -> Result<(), Erro } } + if (opt.default_bucket || opt.default_access_key) && !opt.single_node { + return Err(Error::Message( + "Flags --default-access-key and --default-bucket can only be used in single-node mode." + .into(), + )); + } + + if opt.default_access_key || opt.default_bucket { + let rdenv = |name: &str| { + std::env::var(name) + .map_err(|_| Error::Message(format!("Environment variable `{}` is not set", name))) + }; + + // Create default access key if it does not exist + let key_id = rdenv("GARAGE_DEFAULT_ACCESS_KEY")?; + let secret_key = rdenv("GARAGE_DEFAULT_SECRET_KEY")?; + + let existing_key = garage.key_table.get(&EmptyKey, &key_id).await?; + + let key = match existing_key { + Some(key) => { + match key.state.as_option() { + None => return Err(Error::Message(format!("Access key {} was deleted in the cluster, cannot add it back", key_id))), + Some(st) if st.secret_key != secret_key => return Err(Error::Message(format!("Access key {} is associated with a secret key different than the one given in GARAGE_DEFAULT_SECRET_KEY", key_id))), + _ => (), + } + + key + } + None => { + info!("Creating default access key `{}`", key_id); + + let mut key = Key::import(&key_id, &secret_key, "default access key") + .map_err(|e| Error::Message(format!("Invalid default access key: {}", e)))?; + key.state + .as_option_mut() + .unwrap() + .allow_create_bucket + .update(true); + garage.key_table.insert(&key).await?; + + key + } + }; + + if opt.default_bucket { + // Create default bucket if it does not exist + let bucket_name = rdenv("GARAGE_DEFAULT_BUCKET")?; + + if !is_valid_bucket_name(&bucket_name, garage.config.allow_punycode) { + return Err(Error::Message( + "Invalid default bucket name, see S3 specification for allowed bucket names." + .into(), + )); + } + + let helper = garage.locked_helper().await; + + let bucket = match helper.bucket().resolve_global_bucket_fast(&bucket_name)? { + Some(bucket) => bucket, + None => { + info!("Creating default bucket `{}`", bucket_name); + + let bucket = Bucket::new(); + garage.bucket_table.insert(&bucket).await?; + + helper + .set_global_bucket_alias(bucket.id, &bucket_name) + .await + .map_err(|e| { + Error::Message(format!("Cannot create default bucket: {}", e)) + })?; + + bucket + } + }; + + helper + .set_bucket_key_permissions( + bucket.id, + &key.key_id, + BucketKeyPerm { + timestamp: now_msec(), + allow_read: true, + allow_write: true, + allow_owner: true, + }, + ) + .await + .map_err(|e| { + Error::Message(format!( + "Cannot configure permissions on default bucket: {}", + e + )) + })?; + } + } + Ok(()) }