diff --git a/Cargo.lock b/Cargo.lock index b64ed8b25..0155b9c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,9 +348,9 @@ dependencies = [ [[package]] name = "cloudflare" -version = "0.8.7" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10114a706e77bc0115b7c67db19c5c5544dd2eabef992d5772d2f369567e71a5" +checksum = "2c17127dbdf8ba8654a984ed7eb7d5077e137c30e34d8c0a4212753426ea6f1b" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 8628e3af5..0fc04d129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ binary-install = "0.0.3-alpha.1" chrome-devtools-rs = { version = "0.0.0-alpha.3", features = ["color"] } chrono = "0.4.19" clap = "2.33.3" -cloudflare = "0.8.3" +cloudflare = "0.9.0" colored_json = "2.1.0" config = { version = "0.11.0", default-features = false, features = ["toml", "json", "yaml", "ini"] } console = "0.14.1" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index b19aa2446..361d8521d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -8,6 +8,7 @@ pub mod login; pub mod logout; pub mod preview; pub mod publish; +pub mod r2; pub mod route; pub mod secret; pub mod subdomain; @@ -27,6 +28,7 @@ pub mod exec { pub use super::logout::logout; pub use super::preview::preview; pub use super::publish::publish; + pub use super::r2::r2_bucket; pub use super::route::route; pub use super::secret::secret; pub use super::subdomain::subdomain; @@ -89,6 +91,10 @@ pub enum Command { #[structopt(name = "kv:bulk", setting = AppSettings::SubcommandRequiredElseHelp)] KvBulk(kv::KvBulk), + /// Interact with your Workers R2 Buckets + #[structopt(setting = AppSettings::SubcommandRequiredElseHelp)] + R2(r2::R2), + /// List or delete worker routes. #[structopt(name = "route", setting = AppSettings::SubcommandRequiredElseHelp)] Route(route::Route), diff --git a/src/cli/r2.rs b/src/cli/r2.rs new file mode 100644 index 000000000..bb5133426 --- /dev/null +++ b/src/cli/r2.rs @@ -0,0 +1,45 @@ +use super::Cli; +use crate::commands; +use crate::settings::{global_user::GlobalUser, toml::Manifest}; + +use anyhow::Result; +use structopt::StructOpt; + +#[derive(Debug, Clone, StructOpt)] +#[structopt(rename_all = "lower")] +pub enum R2 { + /// Interact with your Workers R2 Buckets + Bucket(Bucket), +} + +#[derive(Debug, Clone, StructOpt)] +#[structopt(rename_all = "lower")] +pub enum Bucket { + /// List existing buckets + List, + /// Create a new bucket + Create { + /// The name for your new bucket + #[structopt(index = 1)] + name: String, + }, + /// Delete an existing bucket + Delete { + /// The name of the bucket to delete + /// Note: bucket must be empty + #[structopt(index = 1)] + name: String, + }, +} + +pub fn r2_bucket(r2: R2, cli_params: &Cli) -> Result<()> { + let user = GlobalUser::new()?; + let manifest = Manifest::new(&cli_params.config)?; + let env = cli_params.environment.as_deref(); + + match r2 { + R2::Bucket(Bucket::List) => commands::r2::list(&manifest, env, &user), + R2::Bucket(Bucket::Create { name }) => commands::r2::create(&manifest, env, &user, &name), + R2::Bucket(Bucket::Delete { name }) => commands::r2::delete(&manifest, env, &user, &name), + } +} diff --git a/src/commands/kv/mod.rs b/src/commands/kv/mod.rs index 534b599fd..d347a87e9 100644 --- a/src/commands/kv/mod.rs +++ b/src/commands/kv/mod.rs @@ -103,6 +103,7 @@ mod tests { binding: "KV".to_string(), }, ], + r2_buckets: Vec::new(), durable_objects: None, migrations: None, name: "test-target".to_string(), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 888d566d3..47191bbfe 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ pub mod login; pub mod logout; mod preview; pub mod publish; +pub mod r2; pub mod report; pub mod route; pub mod secret; diff --git a/src/commands/publish.rs b/src/commands/publish.rs index 4b4a7ed92..a426826c7 100644 --- a/src/commands/publish.rs +++ b/src/commands/publish.rs @@ -182,6 +182,16 @@ fn validate_target_required_fields_present(target: &Target) -> Result<()> { } } + for r2 in &target.r2_buckets { + if r2.binding.is_empty() { + missing_fields.push("r2-bucket binding") + } + + if r2.bucket_name.is_empty() { + missing_fields.push("r2-bucket bucket_name") + } + } + let (field_pluralization, is_are) = match missing_fields.len() { n if n >= 2 => ("fields", "are"), 1 => ("field", "is"), diff --git a/src/commands/r2.rs b/src/commands/r2.rs new file mode 100644 index 000000000..67d3ba1c7 --- /dev/null +++ b/src/commands/r2.rs @@ -0,0 +1,74 @@ +use anyhow::Result; + +use crate::http; +use crate::settings::global_user::GlobalUser; +use crate::settings::toml::Manifest; +use crate::terminal::message::{Message, StdOut}; + +use cloudflare::endpoints::r2::{CreateBucket, DeleteBucket, ListBuckets}; +use cloudflare::framework::apiclient::ApiClient; + +pub fn list(manifest: &Manifest, env: Option<&str>, user: &GlobalUser) -> Result<()> { + let account_id = manifest.get_account_id(env)?; + let client = http::cf_v4_client(user)?; + let result = client.request(&ListBuckets { + account_identifier: &account_id, + }); + + match result { + Ok(response) => { + let buckets: Vec = response + .result + .buckets + .into_iter() + .map(|b| b.name) + .collect(); + println!("{:?}", buckets); + } + Err(e) => println!("{}", e), + } + + Ok(()) +} + +pub fn create(manifest: &Manifest, env: Option<&str>, user: &GlobalUser, name: &str) -> Result<()> { + let account_id = manifest.get_account_id(env)?; + let msg = format!("Creating bucket \"{}\"", name); + StdOut::working(&msg); + + let client = http::cf_v4_client(user)?; + let result = client.request(&CreateBucket { + account_identifier: &account_id, + bucket_name: name, + }); + + match result { + Ok(_) => { + StdOut::success("Success!"); + } + Err(e) => print!("{}", e), + } + + Ok(()) +} + +pub fn delete(manifest: &Manifest, env: Option<&str>, user: &GlobalUser, name: &str) -> Result<()> { + let account_id = manifest.get_account_id(env)?; + let msg = format!("Deleting bucket \"{}\"", name); + StdOut::working(&msg); + + let client = http::cf_v4_client(user)?; + let result = client.request(&DeleteBucket { + account_identifier: &account_id, + bucket_name: name, + }); + + match result { + Ok(_) => { + StdOut::success("Success!"); + } + Err(e) => print!("{}", e), + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 6ed50df16..40f325893 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,6 +89,7 @@ fn run() -> Result<()> { Command::Subdomain { name } => exec::subdomain(name, &cli_params), Command::Route(route) => exec::route(route, &cli_params), Command::Secret(secret) => exec::secret(secret, &cli_params), + Command::R2(r2) => exec::r2_bucket(r2, &cli_params), Command::KvNamespace(namespace) => exec::kv_namespace(namespace, &cli_params), Command::KvKey(key) => exec::kv_key(key, &cli_params), Command::KvBulk(bulk) => exec::kv_bulk(bulk, &cli_params), diff --git a/src/settings/binding.rs b/src/settings/binding.rs index d8015d1f5..f2ca5e1f6 100644 --- a/src/settings/binding.rs +++ b/src/settings/binding.rs @@ -12,6 +12,10 @@ pub enum Binding { name: String, namespace_id: String, }, + R2Bucket { + name: String, + bucket_name: String, + }, #[serde(rename = "durable_object_namespace")] DurableObjectsClass { name: String, @@ -37,6 +41,10 @@ impl Binding { Binding::KvNamespace { name, namespace_id } } + pub fn new_r2_bucket(name: String, bucket_name: String) -> Binding { + Binding::R2Bucket { name, bucket_name } + } + pub fn new_durable_object_namespace( name: String, class_name: String, diff --git a/src/settings/toml/environment.rs b/src/settings/toml/environment.rs index 82aa30083..c49dcdd75 100644 --- a/src/settings/toml/environment.rs +++ b/src/settings/toml/environment.rs @@ -7,6 +7,7 @@ use serde_with::rust::string_empty_as_none; use crate::settings::toml::builder::Builder; use crate::settings::toml::durable_objects::DurableObjects; use crate::settings::toml::kv_namespace::ConfigKvNamespace; +use crate::settings::toml::r2_bucket::ConfigR2Bucket; use crate::settings::toml::route::RouteConfig; use crate::settings::toml::site::Site; use crate::settings::toml::triggers::Triggers; @@ -28,6 +29,7 @@ pub struct Environment { pub site: Option, #[serde(alias = "kv-namespaces")] pub kv_namespaces: Option>, + pub r2_buckets: Option>, pub vars: Option>, pub text_blobs: Option>, pub triggers: Option, diff --git a/src/settings/toml/manifest.rs b/src/settings/toml/manifest.rs index 1b3dfd07b..11e9547ec 100644 --- a/src/settings/toml/manifest.rs +++ b/src/settings/toml/manifest.rs @@ -23,6 +23,7 @@ use crate::settings::toml::dev::Dev; use crate::settings::toml::durable_objects::DurableObjects; use crate::settings::toml::environment::Environment; use crate::settings::toml::kv_namespace::{ConfigKvNamespace, KvNamespace}; +use crate::settings::toml::r2_bucket::{ConfigR2Bucket, R2Bucket}; use crate::settings::toml::route::RouteConfig; use crate::settings::toml::site::Site; use crate::settings::toml::target_type::TargetType; @@ -63,6 +64,7 @@ pub struct Manifest { pub env: Option>, #[serde(alias = "kv-namespaces")] pub kv_namespaces: Option>, + pub r2_buckets: Option>, // TODO: maybe one day, serde toml support will allow us to serialize sites // as a TOML inline table (this would prevent confusion with environments too!) pub site: Option, @@ -374,6 +376,7 @@ impl Manifest { // to include the name of the environment name: self.name.clone(), // Inherited kv_namespaces: get_namespaces(self.kv_namespaces.clone(), preview)?, // Not inherited + r2_buckets: get_buckets(self.r2_buckets.clone(), preview)?, // Not inherited durable_objects: self.durable_objects.clone(), // Not inherited migrations: match (preview, &self.migrations) { (false, Some(migrations)) => Some(Migrations::List { @@ -408,6 +411,9 @@ impl Manifest { // don't inherit kv namespaces because it is an anti-pattern to use the same namespaces across multiple environments target.kv_namespaces = get_namespaces(environment.kv_namespaces.clone(), preview)?; + // don't inherit r2 buckets because it is an anti-pattern to use the same buckets across multiple environments + target.r2_buckets = get_buckets(environment.r2_buckets.clone(), preview)?; + // don't inherit durable object configuration target.durable_objects = environment.durable_objects.clone(); @@ -750,6 +756,37 @@ fn get_namespaces( } } +fn get_buckets(r2_buckets: Option>, preview: bool) -> Result> { + if let Some(buckets) = r2_buckets { + buckets.into_iter().map(|ns| { + if preview { + if let Some(preview_bucket_name) = &ns.preview_bucket_name { + if let Some(bucket_name) = &ns.bucket_name { + if preview_bucket_name == bucket_name { + StdOut::warn("Specifying the same r2 bucket_name for both preview and production sessions may cause bugs in your production worker! Proceed with caution."); + } + } + Ok(R2Bucket { + bucket_name: preview_bucket_name.to_string(), + binding: ns.binding.to_string(), + }) + } else { + anyhow::bail!("In order to preview a worker with r2 buckets, you must designate a preview_bucket_name in your configuration file for each r2 bucket you'd like to preview.") + } + } else if let Some(bucket_name) = &ns.bucket_name { + Ok(R2Bucket { + bucket_name: bucket_name.to_string(), + binding: ns.binding, + }) + } else { + anyhow::bail!("You must specify the bucket name in the bucket_name field for the bucket with binding \"{}\"", &ns.binding) + } + }).collect() + } else { + Ok(Vec::new()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/settings/toml/mod.rs b/src/settings/toml/mod.rs index 47c4efcbe..2c2deefd2 100644 --- a/src/settings/toml/mod.rs +++ b/src/settings/toml/mod.rs @@ -5,6 +5,7 @@ mod environment; mod kv_namespace; mod manifest; pub mod migrations; +mod r2_bucket; mod route; mod site; pub(crate) mod target; @@ -15,6 +16,7 @@ pub use builder::{ModuleRule, UploadFormat}; pub use durable_objects::{DurableObjects, DurableObjectsClass}; pub use kv_namespace::{ConfigKvNamespace, KvNamespace}; pub use manifest::Manifest; +pub use r2_bucket::{ConfigR2Bucket, R2Bucket}; pub use route::{Route, RouteConfig}; pub use site::Site; pub use target::Target; diff --git a/src/settings/toml/r2_bucket.rs b/src/settings/toml/r2_bucket.rs new file mode 100644 index 000000000..2c251f0b3 --- /dev/null +++ b/src/settings/toml/r2_bucket.rs @@ -0,0 +1,34 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::settings::binding::Binding; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ConfigR2Bucket { + pub binding: String, + pub bucket_name: Option, + pub preview_bucket_name: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct R2Bucket { + pub binding: String, + pub bucket_name: String, +} + +impl fmt::Display for R2Bucket { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "binding: {}, bucket_name: {}", + self.binding, self.bucket_name + ) + } +} + +impl R2Bucket { + pub fn binding(&self) -> Binding { + Binding::new_r2_bucket(self.binding.clone(), self.bucket_name.clone()) + } +} diff --git a/src/settings/toml/target.rs b/src/settings/toml/target.rs index be131852d..4509cdd1d 100644 --- a/src/settings/toml/target.rs +++ b/src/settings/toml/target.rs @@ -1,6 +1,7 @@ use super::durable_objects::DurableObjects; use super::kv_namespace::KvNamespace; pub(crate) use super::manifest::LazyAccountId; +use super::r2_bucket::R2Bucket; use super::site::Site; use super::target_type::TargetType; use super::UsageModel; @@ -15,6 +16,7 @@ use std::path::PathBuf; pub struct Target { pub account_id: LazyAccountId, pub kv_namespaces: Vec, + pub r2_buckets: Vec, pub durable_objects: Option, pub migrations: Option, pub name: String, diff --git a/src/settings/toml/tests/mod.rs b/src/settings/toml/tests/mod.rs index 2a33c69bc..44fd8db2f 100644 --- a/src/settings/toml/tests/mod.rs +++ b/src/settings/toml/tests/mod.rs @@ -78,6 +78,51 @@ fn it_builds_from_environments_config_with_kv() { } } +#[test] +fn it_builds_from_environments_config_with_r2() { + let toml_path = toml_fixture_path("r2_buckets"); + + let manifest = Manifest::new(&toml_path).unwrap(); + + let target = manifest.get_target(None, false).unwrap(); + assert!(target.r2_buckets.is_empty()); + + let target = manifest.get_target(Some("production"), false).unwrap(); + let r2_1 = R2Bucket { + bucket_name: "long_bucket_name".to_string(), + binding: "prodr2_1".to_string(), + }; + let r2_2 = R2Bucket { + bucket_name: "another_long_bucket_name".to_string(), + binding: "prodr2_2".to_string(), + }; + + if target.r2_buckets.is_empty() { + panic!("found no r2 buckets"); + } else { + assert_eq!(target.r2_buckets.len(), 2); + assert!(target.r2_buckets.contains(&r2_1)); + assert!(target.r2_buckets.contains(&r2_2)); + } + + let target = manifest.get_target(Some("staging"), false).unwrap(); + let r2_1 = R2Bucket { + bucket_name: "long_bucket_name".to_string(), + binding: "stagingr2_1".to_string(), + }; + let r2_2 = R2Bucket { + bucket_name: "another_long_bucket_name".to_string(), + binding: "stagingr2_2".to_string(), + }; + if target.r2_buckets.is_empty() { + panic!("found no r2 buckets"); + } else { + assert_eq!(target.r2_buckets.len(), 2); + assert!(target.r2_buckets.contains(&r2_1)); + assert!(target.r2_buckets.contains(&r2_2)); + } +} + #[test] fn parses_same_from_config_path_as_string() { env::remove_var("CF_ACCOUNT_ID"); diff --git a/src/settings/toml/tests/tomls/r2_buckets.toml b/src/settings/toml/tests/tomls/r2_buckets.toml new file mode 100644 index 000000000..574682a6d --- /dev/null +++ b/src/settings/toml/tests/tomls/r2_buckets.toml @@ -0,0 +1,33 @@ +type = "webpack" +name = "worker" +zone_id = "" +account_id = "" +route = "dev.example.com/*" + +[env.production] +name = "production-worker" +zone_id = "" +account_id = "" +route = "example.com/*" + +[[env.production.r2_buckets]] +bucket_name = "long_bucket_name" +binding = "prodr2_1" + +[[env.production.r2_buckets]] +bucket_name = "another_long_bucket_name" +binding = "prodr2_2" + +[env.staging] +name = "staging-worker" +zone_id = "" +account_id = "" +route = "staging.example.com/*" + +[[env.staging.r2_buckets]] +bucket_name = "long_bucket_name" +binding = "stagingr2_1" + +[[env.staging.r2_buckets]] +bucket_name = "another_long_bucket_name" +binding = "stagingr2_2" diff --git a/src/sites/mod.rs b/src/sites/mod.rs index c66f72165..509f4b740 100644 --- a/src/sites/mod.rs +++ b/src/sites/mod.rs @@ -333,6 +333,7 @@ mod tests { Target { account_id: None.into(), kv_namespaces: Vec::new(), + r2_buckets: Vec::new(), durable_objects: None, migrations: None, name: "".to_string(), diff --git a/src/upload/form/mod.rs b/src/upload/form/mod.rs index 1ad3ca157..f72aa395a 100644 --- a/src/upload/form/mod.rs +++ b/src/upload/form/mod.rs @@ -34,6 +34,7 @@ pub fn build( let compatibility_date = target.compatibility_date.clone(); let compatibility_flags = target.compatibility_flags.clone(); let kv_namespaces = &target.kv_namespaces; + let r2_buckets = &target.r2_buckets; let durable_object_classes = target .durable_objects .as_ref() @@ -91,6 +92,7 @@ pub fn build( compatibility_flags, wasm_modules, kv_namespaces: kv_namespaces.to_vec(), + r2_buckets: r2_buckets.to_vec(), durable_object_classes, text_blobs, plain_texts, @@ -113,6 +115,7 @@ pub fn build( compatibility_flags, wasm_modules, kv_namespaces: kv_namespaces.to_vec(), + r2_buckets: r2_buckets.to_vec(), durable_object_classes, text_blobs, plain_texts, @@ -133,6 +136,7 @@ pub fn build( compatibility_flags, module_config.get_modules()?, kv_namespaces.to_vec(), + r2_buckets.to_vec(), durable_object_classes, migration, text_blobs, @@ -155,6 +159,7 @@ pub fn build( compatibility_flags, wasm_modules, kv_namespaces: kv_namespaces.to_vec(), + r2_buckets: r2_buckets.to_vec(), durable_object_classes, text_blobs, plain_texts, @@ -185,6 +190,7 @@ pub fn build( compatibility_flags, wasm_modules, kv_namespaces: kv_namespaces.to_vec(), + r2_buckets: r2_buckets.to_vec(), durable_object_classes, text_blobs, plain_texts, diff --git a/src/upload/form/project_assets.rs b/src/upload/form/project_assets.rs index 99aefef90..fe646cff0 100644 --- a/src/upload/form/project_assets.rs +++ b/src/upload/form/project_assets.rs @@ -14,7 +14,7 @@ use super::wasm_module::WasmModule; use super::UsageModel; use crate::settings::toml::{ - migrations::ApiMigration, DurableObjectsClass, KvNamespace, ModuleRule, + migrations::ApiMigration, DurableObjectsClass, KvNamespace, ModuleRule, R2Bucket, }; use std::collections::{HashMap, HashSet}; @@ -25,6 +25,7 @@ pub struct ServiceWorkerAssets { pub compatibility_flags: Vec, pub wasm_modules: Vec, pub kv_namespaces: Vec, + pub r2_buckets: Vec, pub durable_object_classes: Vec, pub text_blobs: Vec, pub plain_texts: Vec, @@ -43,6 +44,10 @@ impl ServiceWorkerAssets { let binding = kv.binding(); bindings.push(binding); } + for r2 in &self.r2_buckets { + let binding = r2.binding(); + bindings.push(binding); + } for do_ns in &self.durable_object_classes { let binding = do_ns.binding(); bindings.push(binding); @@ -316,6 +321,7 @@ pub struct ModulesAssets { pub compatibility_flags: Vec<String>, pub manifest: ModuleManifest, pub kv_namespaces: Vec<KvNamespace>, + pub r2_buckets: Vec<R2Bucket>, pub durable_object_classes: Vec<DurableObjectsClass>, pub migration: Option<ApiMigration>, pub text_blobs: Vec<TextBlob>, @@ -330,6 +336,7 @@ impl ModulesAssets { compatibility_flags: Vec<String>, manifest: ModuleManifest, kv_namespaces: Vec<KvNamespace>, + r2_buckets: Vec<R2Bucket>, durable_object_classes: Vec<DurableObjectsClass>, migration: Option<ApiMigration>, text_blobs: Vec<TextBlob>, @@ -341,6 +348,7 @@ impl ModulesAssets { compatibility_flags, manifest, kv_namespaces, + r2_buckets, durable_object_classes, migration, text_blobs, @@ -359,6 +367,10 @@ impl ModulesAssets { let binding = kv.binding(); bindings.push(binding); } + for r2 in &self.r2_buckets { + let binding = r2.binding(); + bindings.push(binding); + } for class in &self.durable_object_classes { let binding = class.binding(); bindings.push(binding);