Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cosmos] Control-Plane Over Data-Plane APIs (Database/Container CRUD) #1853

Open
wants to merge 7 commits into
base: feature/track2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ base64 = "0.22"
bytes = "1.0"
cargo_metadata = "0.18.1"
clap = { version = "4.4.16", features = ["derive"] }
cfg-if = "1.0.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used cfg-if to simplify some conditional compilation in azure_data_cosmos. It's a relatively low-impact crate, but I'm open to removing it and going back to a series of #[cfg(...)]-attributed definitions if that's preferred. (cc @heaths )

dyn-clone = "1.0"
fe2o3-amqp = { version = "0.12", features = ["native-tls", "tracing", "uuid"] }
fe2o3-amqp-ext = { version = "0.12", features = [] }
Expand Down
6 changes: 3 additions & 3 deletions sdk/cosmos/azure_data_cosmos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ documentation = "https://docs.rs/azure_data_cosmos"
keywords = ["sdk", "azure", "rest", "cloud", "cosmos", "database"]
categories = ["api-bindings"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cfg-if.workspace = true
async-trait.workspace = true
azure_core.workspace = true
typespec_client_core = { workspace = true, features = ["derive"] }
Expand Down Expand Up @@ -44,6 +43,7 @@ default = ["hmac_rust"]
hmac_rust = ["azure_core/hmac_rust"]
hmac_openssl = ["azure_core/hmac_openssl"]
key_auth = [] # Enables support for key-based authentication (Primary Keys and Resource Tokens)
control_plane = ["key_auth"] # Control-plane operations require key-based authentication.

[package.metadata.docs.rs]
features = ["key_auth"]
features = ["control_plane", "key_auth"]
5 changes: 5 additions & 0 deletions sdk/cosmos/azure_data_cosmos/docs/control-plane-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="warning">
analogrelay marked this conversation as resolved.
Show resolved Hide resolved

This is a control-plane API and requires that you authenticate using a key. To use Entra ID to perform this operation, you must use the [Azure Resource Manager APIs](https://learn.microsoft.com/en-us/azure/templates/microsoft.documentdb/databaseaccounts).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PR does the right thing for the point in time we're at, but just pointing out that I believe there is a plan to support these operations via Entra ID at some point in 2025, and also to integrate the dataplane RBAC with normal Azure RBAC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the intent is that this feature can be "removed" later. It will have to remain present, and continue to imply key_auth to avoid breaking users, but the #[cfg] attributes referencing it would go away and the methods related to it would just become available to everyone. At that point users would be able to remove the feature reference from their Cargo.toml (possibly replacing with key_auth if they still need that support)


</div>
141 changes: 114 additions & 27 deletions sdk/cosmos/azure_data_cosmos/examples/cosmos/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,129 @@ use azure_data_cosmos::{
clients::{ContainerClientMethods, DatabaseClientMethods},
CosmosClient, CosmosClientMethods, PartitionKey,
};
use clap::Args;
use clap::{Args, Subcommand};

/// Creates a new item.
#[cfg(feature = "control_plane")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoyingly, it sees we need to conditionally-compile our use statements as well to avoid 'unused import' lints. I'd prefer it if clippy considered all features (maybe we just need to run it with --all-features 🤔 ) since extraneous imports don't impact the functionality, but alas.

use azure_data_cosmos::models::{ContainerProperties, PartitionKeyDefinition};

/// Creates a new item, database, or container.
#[derive(Clone, Args)]
pub struct CreateCommand {
/// The database in which to create the item.
database: String,
#[command(subcommand)]
subcommand: Subcommands,
}

#[derive(Clone, Subcommand)]
pub enum Subcommands {
/// Create an item in a container.
Item {
/// The database in which to create the item.
database: String,

/// The container in which to create the item.
container: String,

/// The partition key of the new item.
#[clap(long, short)]
partition_key: String,

/// The JSON of the new item.
#[clap(long, short)]
json: String,
},

/// The container in which to create the item.
container: String,
/// Create a database (does not support Entra ID).
#[cfg(feature = "control_plane")]
Database {
/// The ID of the new database to create.
id: String,
},

/// The partition key of the new item.
#[clap(long, short)]
partition_key: String,
/// Create a container (does not support Entra ID).
#[cfg(feature = "control_plane")]
Container {
/// The ID of the database to create the container in.
database: String,

/// The JSON of the new item.
#[clap(long, short)]
json: String,
/// The ID of the new container to create.
#[clap(long, short)]
id: Option<String>,

/// The path to the partition key properties (supports up to 3).
#[clap(long, short)]
partition_key: Vec<String>,

/// The JSON for a ContainerProperties value. The 'id' and 'partition key' options are ignored if this is set.
#[clap(long)]
json: Option<String>,
},
}

impl CreateCommand {
pub async fn run(self, client: CosmosClient) -> Result<(), Box<dyn Error>> {
let db_client = client.database_client(&self.database);
let container_client = db_client.container_client(&self.container);

let pk = PartitionKey::from(&self.partition_key);
let item: serde_json::Value = serde_json::from_str(&self.json)?;

let created = container_client
.create_item(pk, item, None)
.await?
.deserialize_body()
.await?
.unwrap();
println!("Created item:");
println!("{:#?}", created);
Ok(())
match self.subcommand {
Subcommands::Item {
database,
container,
partition_key,
json,
} => {
let db_client = client.database_client(database);
let container_client = db_client.container_client(container);

let pk = PartitionKey::from(&partition_key);
let item: serde_json::Value = serde_json::from_str(&json)?;

let created = container_client
.create_item(pk, item, None)
.await?
.deserialize_body()
.await?
.unwrap();
println!("Created item:");
println!("{:#?}", created);
Ok(())
}

#[cfg(feature = "control_plane")]
Subcommands::Database { id } => {
let db = client
.create_database(id, None)
.await?
.deserialize_body()
.await?
.unwrap();
println!("Created database:");
println!("{:#?}", db);
Ok(())
}

#[cfg(feature = "control_plane")]
Subcommands::Container {
database,
id,
partition_key,
json,
} => {
let properties = match json {
Some(j) => serde_json::from_str(&j).unwrap(),
None => ContainerProperties {
id: id.expect("the ID is required when not using '--json'"),
partition_key: PartitionKeyDefinition::new(partition_key),
..Default::default()
},
};
let container = client
.database_client(database)
.create_container(properties, None)
.await?
.deserialize_body()
.await?
.unwrap();
println!("Created container:");
println!("{:#?}", container);
Ok(())
}
}
}
}
100 changes: 76 additions & 24 deletions sdk/cosmos/azure_data_cosmos/examples/cosmos/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,91 @@ use azure_data_cosmos::{
clients::{ContainerClientMethods, DatabaseClientMethods},
CosmosClient, CosmosClientMethods,
};
use clap::Args;
use clap::{Args, Subcommand};

/// Deletes an item.
/// Deletes an item, database, or container.
#[derive(Clone, Args)]
pub struct DeleteCommand {
/// The database containing the item.
database: String,
#[command(subcommand)]
subcommand: Subcommands,
}

#[derive(Clone, Subcommand)]
pub enum Subcommands {
/// Delete an item in a container.
Item {
/// The database containing the item.
database: String,

/// The container containing the item.
container: String,

/// The ID of the item.
#[clap(long, short)]
item_id: String,

/// The container containing the item.
container: String,
/// The partition key of the item.
#[clap(long, short)]
partition_key: String,
},

/// The ID of the item.
#[clap(long, short)]
item_id: String,
/// Create a database (does not support Entra ID).
#[cfg(feature = "control_plane")]
Database {
/// The ID of the database to delete.
id: String,
},

/// The partition key of the item.
#[clap(long, short)]
partition_key: String,
/// Create a container (does not support Entra ID).
#[cfg(feature = "control_plane")]
Container {
/// The ID of the database the container is in.
database: String,

/// The ID of the container to delete
id: String,
},
}

impl DeleteCommand {
pub async fn run(self, client: CosmosClient) -> Result<(), Box<dyn Error>> {
let db_client = client.database_client(&self.database);
let container_client = db_client.container_client(&self.container);

let response = container_client
.delete_item(&self.partition_key, &self.item_id, None)
.await;
match response {
Err(e) if e.http_status() == Some(StatusCode::NotFound) => println!("Item not found!"),
Ok(_) => println!("Item deleted"),
Err(e) => return Err(e.into()),
};
Ok(())
match self.subcommand {
Subcommands::Item {
database,
container,
item_id,
partition_key,
} => {
let db_client = client.database_client(database);
let container_client = db_client.container_client(container);

let response = container_client
.delete_item(partition_key, item_id, None)
.await;
match response {
Err(e) if e.http_status() == Some(StatusCode::NotFound) => {
println!("Item not found!")
}
Ok(_) => println!("Item deleted"),
Err(e) => return Err(e.into()),
};
Ok(())
}

#[cfg(feature = "control_plane")]
Subcommands::Database { id } => {
let db_client = client.database_client(id);
db_client.delete(None).await?;
Ok(())
}

#[cfg(feature = "control_plane")]
Subcommands::Container { database, id } => {
let db_client = client.database_client(database);
let container_client = db_client.container_client(id);
container_client.delete(None).await?;
Ok(())
}
}
}
}
Loading