From 8ec96645d638d36518ee8f97cc28ec8dbb21c56d Mon Sep 17 00:00:00 2001 From: Jack Westwood Date: Fri, 30 Aug 2024 11:37:48 +0100 Subject: [PATCH] Implement collections commands for Capella clusters Implement collections create for Capella clusters Implement collections drop for Capella clusters --- src/cli/collections.rs | 247 ++++++++++++++++++++++------------ src/cli/collections_create.rs | 190 ++++++++++++++++++-------- src/cli/collections_drop.rs | 166 ++++++++++++++++------- src/client/cloud.rs | 179 ++++++++++++++++++++++-- src/client/cloud_json.rs | 32 +++++ tests/common/playground.rs | 12 +- 6 files changed, 626 insertions(+), 200 deletions(-) diff --git a/src/cli/collections.rs b/src/cli/collections.rs index 9d6dd87d..959270ad 100644 --- a/src/cli/collections.rs +++ b/src/cli/collections.rs @@ -1,9 +1,13 @@ -use crate::cli::util::{cluster_identifiers_from, get_active_cluster, NuValueMap}; +use crate::cli::util::{ + cluster_from_conn_str, cluster_identifiers_from, find_org_id, find_project_id, + get_active_cluster, NuValueMap, +}; use crate::client::ManagementRequest; -use crate::state::State; +use crate::state::{RemoteCapellaOrganization, State}; use log::debug; use serde_derive::Deserialize; use std::ops::Add; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time::Instant; @@ -12,12 +16,15 @@ use crate::cli::error::{ client_error_to_shell_error, deserialize_error, no_active_bucket_error, unexpected_status_code_error, }; +use crate::cli::no_active_scope_error; +use crate::client::cloud_json::Collection; +use crate::remote_cluster::RemoteClusterType::Provisioned; use crate::RemoteCluster; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, + Category, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, }; #[derive(Clone)] @@ -51,7 +58,6 @@ impl Command for Collections { "the clusters to query against", None, ) - .switch("all", "include system scopes in the output", Some('a')) .category(Category::Custom("couchbase".to_string())) } @@ -83,90 +89,59 @@ fn collections_get( let cluster_identifiers = cluster_identifiers_from(engine_state, stack, &state, call, true)?; let guard = state.lock().unwrap(); - let scope: Option = call.get_flag(engine_state, stack, "scope")?; - - let display_all = call.has_flag(engine_state, stack, "all")?; - let mut results: Vec = vec![]; for identifier in cluster_identifiers { let active_cluster = get_active_cluster(identifier.clone(), &guard, span)?; let bucket = get_bucket_or_active(active_cluster, engine_state, stack, call)?; + let scope = get_scope_or_active(active_cluster, engine_state, stack, call)?; debug!( "Running collections get for bucket {:?}, scope {:?}", - &bucket, &scope + &bucket.clone(), + &scope ); - let response = active_cluster - .cluster() - .http_client() - .management_request( - ManagementRequest::GetCollections { bucket }, - Instant::now().add(active_cluster.timeouts().management_timeout()), + let collections = if active_cluster.cluster_type() == Provisioned { + get_capella_collections( + identifier.clone(), + guard.named_or_active_org(active_cluster.capella_org())?, + guard.named_or_active_project(active_cluster.project())?, + active_cluster, + bucket.clone(), + scope.clone(), + ctrl_c.clone(), + span, + ) + } else { + get_server_collections( + active_cluster, + bucket.clone(), + scope.clone(), ctrl_c.clone(), + span, ) - .map_err(|e| client_error_to_shell_error(e, span))?; - - let manifest: Manifest = match response.status() { - 200 => serde_json::from_str(response.content()) - .map_err(|e| deserialize_error(e.to_string(), span))?, - _ => { - return Err(unexpected_status_code_error( - response.status(), - response.content(), - span, - )); - } - }; - - for scope_res in manifest.scopes { - if let Some(scope_name) = &scope { - if scope_name != &scope_res.name { - continue; - } - } - let collections = scope_res.collections; - if collections.is_empty() { - let mut collected = NuValueMap::default(); - collected.add_string("scope", scope_res.name.clone(), span); - collected.add_string("collection", "", span); - collected.add( - "max_expiry", - Value::Duration { - val: 0, - internal_span: span, - }, - ); - collected.add_string("cluster", identifier.clone(), span); - results.push(collected.into_value(span)); - continue; - } - - for collection in collections { - if scope_res.name == *"_system" && !display_all { - continue; - } - let mut collected = NuValueMap::default(); - collected.add_string("scope", scope_res.name.clone(), span); - collected.add_string("collection", collection.name, span); - - let expiry = match collection.max_expiry { - -1 => "".to_string(), - 0 => "inherited".to_string(), - _ => format!("{:?}", Duration::from_secs(collection.max_expiry as u64)), - }; - - collected.add( - "max_expiry", - Value::String { - val: expiry, - internal_span: span, - }, - ); - collected.add_string("cluster", identifier.clone(), span); - results.push(collected.into_value(span)); - } + }?; + + for collection in collections { + let mut collected = NuValueMap::default(); + collected.add_string("collection", collection.name(), span); + + let expiry = match collection.max_expiry() { + -1 => "".to_string(), + 0 => "inherited".to_string(), + _ => format!("{:?}", Duration::from_secs(collection.max_expiry() as u64)), + }; + + collected.add( + "max_expiry", + Value::String { + val: expiry, + internal_span: span, + }, + ); + collected.add_string("cluster", identifier.clone(), span); + results.push(collected.into_value(span)); } } @@ -192,20 +167,126 @@ pub fn get_bucket_or_active( } } -#[derive(Debug, Deserialize)] -pub struct ManifestCollection { - pub name: String, - #[serde(rename = "maxTTL")] - pub max_expiry: i64, +pub fn get_scope_or_active( + active_cluster: &RemoteCluster, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + match call.get_flag(engine_state, stack, "scope")? { + Some(v) => Ok(v), + None => match active_cluster.active_scope() { + Some(s) => Ok(s), + None => Err(no_active_scope_error(call.span())), + }, + } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct ManifestScope { pub name: String, - pub collections: Vec, + pub collections: Vec, +} + +impl ManifestScope { + pub fn collections(&self) -> Vec { + self.collections.clone() + } } #[derive(Debug, Deserialize)] pub struct Manifest { pub scopes: Vec, } + +impl Manifest { + pub fn scopes(&self) -> Vec { + self.scopes.clone() + } +} + +fn get_capella_collections( + identifier: String, + org: &RemoteCapellaOrganization, + project: String, + cluster: &RemoteCluster, + bucket: String, + scope: String, + ctrl_c: Arc, + span: Span, +) -> Result, ShellError> { + let client = org.client(); + let deadline = Instant::now().add(org.timeout()); + + let org_id = find_org_id(ctrl_c.clone(), &client, deadline, span)?; + let project_id = find_project_id( + ctrl_c.clone(), + project, + &client, + deadline, + span, + org_id.clone(), + )?; + + let json_cluster = cluster_from_conn_str( + identifier.clone(), + ctrl_c.clone(), + cluster.hostnames().clone(), + &client, + deadline, + span, + org_id.clone(), + project_id.clone(), + )?; + + let collections = client + .list_collections( + org_id, + project_id, + json_cluster.id(), + bucket, + scope, + deadline, + ctrl_c, + ) + .map_err(|e| client_error_to_shell_error(e, span))?; + + Ok(collections.items()) +} + +fn get_server_collections( + cluster: &RemoteCluster, + bucket: String, + scope: String, + ctrl_c: Arc, + span: Span, +) -> Result, ShellError> { + let response = cluster + .cluster() + .http_client() + .management_request( + ManagementRequest::GetCollections { bucket }, + Instant::now().add(cluster.timeouts().management_timeout()), + ctrl_c.clone(), + ) + .map_err(|e| client_error_to_shell_error(e, span))?; + + let manifest: Manifest = match response.status() { + 200 => serde_json::from_str(response.content()) + .map_err(|e| deserialize_error(e.to_string(), span))?, + _ => { + return Err(unexpected_status_code_error( + response.status(), + response.content(), + span, + )); + } + }; + + Ok(manifest + .scopes() + .into_iter() + .find(|s| s.name == scope) + .map(|scp| scp.collections()) + .unwrap_or(vec![])) +} diff --git a/src/cli/collections_create.rs b/src/cli/collections_create.rs index 2f88741d..01ccee72 100644 --- a/src/cli/collections_create.rs +++ b/src/cli/collections_create.rs @@ -1,23 +1,28 @@ //! The `collections get` command fetches all of the collection names from the server. -use crate::cli::util::{cluster_identifiers_from, get_active_cluster}; +use crate::cli::util::{ + cluster_from_conn_str, cluster_identifiers_from, find_org_id, find_project_id, + get_active_cluster, +}; use crate::client::ManagementRequest::CreateCollection; -use crate::state::State; +use crate::state::{RemoteCapellaOrganization, State}; use log::debug; use std::ops::Add; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use tokio::time::Instant; -use crate::cli::collections::get_bucket_or_active; +use crate::cli::collections::{get_bucket_or_active, get_scope_or_active}; use crate::cli::error::{ - client_error_to_shell_error, no_active_scope_error, serialize_error, - unexpected_status_code_error, + client_error_to_shell_error, serialize_error, unexpected_status_code_error, }; +use crate::client::cloud_json::Collection; +use crate::remote_cluster::RemoteCluster; +use crate::remote_cluster::RemoteClusterType::Provisioned; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::Value::Nothing; -use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Span, SyntaxShape}; #[derive(Clone)] pub struct CollectionsCreate { @@ -96,61 +101,136 @@ fn collections_create( let active_cluster = get_active_cluster(identifier.clone(), &guard, span)?; let bucket = get_bucket_or_active(active_cluster, engine_state, stack, call)?; - - let scope_name = match call.get_flag(engine_state, stack, "scope")? { - Some(name) => name, - None => match active_cluster.active_scope() { - Some(s) => s, - None => { - return Err(no_active_scope_error(span)); - } - }, - }; + let scope = get_scope_or_active(active_cluster, engine_state, stack, call)?; debug!( "Running collections create for {:?} on bucket {:?}, scope {:?}", - &collection, &bucket, &scope_name + &collection, &bucket, &scope ); - let mut form = vec![("name", collection.clone())]; - if expiry > 0 { - form.push(("maxTTL", expiry.to_string())); - } - - let form_encoded = - serde_urlencoded::to_string(&form).map_err(|e| serialize_error(e.to_string(), span))?; - - let response = active_cluster - .cluster() - .http_client() - .management_request( - CreateCollection { - scope: scope_name, - bucket, - payload: form_encoded, - }, - Instant::now().add(active_cluster.timeouts().management_timeout()), + if active_cluster.cluster_type() == Provisioned { + create_capella_collection( + guard.named_or_active_org(active_cluster.capella_org())?, + guard.named_or_active_project(active_cluster.project())?, + active_cluster, + bucket.clone(), + scope.clone(), + collection.clone(), + expiry, + identifier.clone(), ctrl_c.clone(), + span, ) - .map_err(|e| client_error_to_shell_error(e, span))?; - - match response.status() { - 200 => {} - 202 => {} - _ => { - return Err(unexpected_status_code_error( - response.status(), - response.content(), - span, - )); - } - } + } else { + create_server_collection( + active_cluster, + scope.clone(), + bucket.clone(), + collection.clone(), + expiry, + ctrl_c.clone(), + span, + ) + }? } - Ok(PipelineData::Value( - Nothing { - internal_span: span, - }, - None, - )) + Ok(PipelineData::empty()) +} + +#[allow(clippy::too_many_arguments)] +fn create_capella_collection( + org: &RemoteCapellaOrganization, + project: String, + cluster: &RemoteCluster, + bucket: String, + scope: String, + collection: String, + expiry: i64, + identifier: String, + ctrl_c: Arc, + span: Span, +) -> Result<(), ShellError> { + let client = org.client(); + let deadline = Instant::now().add(org.timeout()); + + let org_id = find_org_id(ctrl_c.clone(), &client, deadline, span)?; + + let project_id = find_project_id( + ctrl_c.clone(), + project, + &client, + deadline, + span, + org_id.clone(), + )?; + + let json_cluster = cluster_from_conn_str( + identifier.clone(), + ctrl_c.clone(), + cluster.hostnames().clone(), + &client, + deadline, + span, + org_id.clone(), + project_id.clone(), + )?; + + let payload = serde_json::to_string(&Collection::new(collection, expiry)).unwrap(); + + client + .create_collection( + org_id, + project_id, + json_cluster.id(), + bucket, + scope, + payload, + deadline, + ctrl_c, + ) + .map_err(|e| client_error_to_shell_error(e, span)) +} + +fn create_server_collection( + cluster: &RemoteCluster, + scope: String, + bucket: String, + collection: String, + expiry: i64, + ctrl_c: Arc, + span: Span, +) -> Result<(), ShellError> { + let mut form = vec![("name", collection.clone())]; + if expiry > 0 { + form.push(("maxTTL", expiry.to_string())); + } + + let form_encoded = + serde_urlencoded::to_string(&form).map_err(|e| serialize_error(e.to_string(), span))?; + + let response = cluster + .cluster() + .http_client() + .management_request( + CreateCollection { + scope, + bucket, + payload: form_encoded, + }, + Instant::now().add(cluster.timeouts().management_timeout()), + ctrl_c.clone(), + ) + .map_err(|e| client_error_to_shell_error(e, span))?; + + match response.status() { + 200 => Ok(()), + 202 => Ok(()), + _ => { + return Err(unexpected_status_code_error( + response.status(), + response.content(), + span, + )); + } + } } diff --git a/src/cli/collections_drop.rs b/src/cli/collections_drop.rs index 0ebc433e..c57555f5 100644 --- a/src/cli/collections_drop.rs +++ b/src/cli/collections_drop.rs @@ -1,22 +1,25 @@ //! The `collections drop` commanddrop a collection from the server. -use crate::cli::util::{cluster_identifiers_from, get_active_cluster}; +use crate::cli::util::{ + cluster_from_conn_str, cluster_identifiers_from, find_org_id, find_project_id, + get_active_cluster, +}; use crate::client::ManagementRequest::DropCollection; -use crate::state::State; +use crate::state::{RemoteCapellaOrganization, State}; use log::debug; use std::ops::Add; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use tokio::time::Instant; -use crate::cli::collections::get_bucket_or_active; -use crate::cli::error::{ - client_error_to_shell_error, no_active_scope_error, unexpected_status_code_error, -}; +use crate::cli::collections::{get_bucket_or_active, get_scope_or_active}; +use crate::cli::error::{client_error_to_shell_error, unexpected_status_code_error}; +use crate::remote_cluster::RemoteCluster; +use crate::remote_cluster::RemoteClusterType::Provisioned; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::Value::Nothing; -use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Span, SyntaxShape}; #[derive(Clone)] pub struct CollectionsDrop { @@ -86,52 +89,121 @@ fn collections_drop( let active_cluster = get_active_cluster(identifier.clone(), &guard, span)?; let bucket = get_bucket_or_active(active_cluster, engine_state, stack, call)?; - - let scope_name = match call.get_flag(engine_state, stack, "scope")? { - Some(name) => name, - None => match active_cluster.active_scope() { - Some(s) => s, - None => { - return Err(no_active_scope_error(span)); - } - }, - }; + let scope = get_scope_or_active(active_cluster, engine_state, stack, call)?; debug!( "Running collections drop for {:?} on bucket {:?}, scope {:?}", - &collection, &bucket, &scope_name + &collection, &bucket, &scope ); - let response = active_cluster - .cluster() - .http_client() - .management_request( - DropCollection { - scope: scope_name, - bucket, - name: collection.clone(), - }, - Instant::now().add(active_cluster.timeouts().management_timeout()), + if active_cluster.cluster_type() == Provisioned { + drop_capella_collection( + guard.named_or_active_org(active_cluster.capella_org())?, + guard.named_or_active_project(active_cluster.project())?, + active_cluster, + bucket.clone(), + scope.clone(), + collection.clone(), + identifier, ctrl_c.clone(), + span, ) - .map_err(|e| client_error_to_shell_error(e, span))?; - - match response.status() { - 200 => {} - _ => { - return Err(unexpected_status_code_error( - response.status(), - response.content(), - span, - )); - } - } + } else { + drop_server_collection( + active_cluster, + bucket.clone(), + scope.clone(), + collection.clone(), + span, + ctrl_c.clone(), + ) + }?; } - Ok(PipelineData::Value( - Nothing { - internal_span: span, - }, - None, - )) + Ok(PipelineData::empty()) +} + +#[allow(clippy::too_many_arguments)] +fn drop_capella_collection( + org: &RemoteCapellaOrganization, + project: String, + cluster: &RemoteCluster, + bucket: String, + scope: String, + collection: String, + identifier: String, + ctrl_c: Arc, + span: Span, +) -> Result<(), ShellError> { + let client = org.client(); + let deadline = Instant::now().add(org.timeout()); + + let org_id = find_org_id(ctrl_c.clone(), &client, deadline, span)?; + + let project_id = find_project_id( + ctrl_c.clone(), + project, + &client, + deadline, + span, + org_id.clone(), + )?; + + let json_cluster = cluster_from_conn_str( + identifier.clone(), + ctrl_c.clone(), + cluster.hostnames().clone(), + &client, + deadline, + span, + org_id.clone(), + project_id.clone(), + )?; + + client + .delete_collection( + org_id, + project_id, + json_cluster.id(), + bucket, + scope, + collection, + deadline, + ctrl_c, + ) + .map_err(|e| client_error_to_shell_error(e, span)) +} + +fn drop_server_collection( + cluster: &RemoteCluster, + bucket: String, + scope: String, + collection: String, + span: Span, + ctrl_c: Arc, +) -> Result<(), ShellError> { + let response = cluster + .cluster() + .http_client() + .management_request( + DropCollection { + scope, + bucket, + name: collection, + }, + Instant::now().add(cluster.timeouts().management_timeout()), + ctrl_c.clone(), + ) + .map_err(|e| client_error_to_shell_error(e, span))?; + + match response.status() { + 200 => Ok(()), + _ => { + return Err(unexpected_status_code_error( + response.status(), + response.content(), + span, + )); + } + } } diff --git a/src/client/cloud.rs b/src/client/cloud.rs index 9ff0d18f..06e2280a 100644 --- a/src/client/cloud.rs +++ b/src/client/cloud.rs @@ -1,7 +1,7 @@ use crate::cli::CtrlcFuture; use crate::client::cloud_json::{ - Bucket, BucketsResponse, Cluster, ClustersResponse, OrganizationsResponse, ProjectsResponse, - ScopesResponse, + Bucket, BucketsResponse, Cluster, ClustersResponse, CollectionsResponse, OrganizationsResponse, + ProjectsResponse, ScopesResponse, }; use crate::client::error::ClientError; use crate::client::http_handler::{HttpResponse, HttpVerb}; @@ -596,13 +596,107 @@ impl CapellaClient { Ok(resp) } + #[allow(clippy::too_many_arguments)] + pub fn create_collection( + &self, + org_id: String, + project_id: String, + cluster_id: String, + bucket: String, + scope: String, + payload: String, + deadline: Instant, + ctrl_c: Arc, + ) -> Result<(), ClientError> { + let request = CapellaRequest::CollectionCreate { + org_id, + project_id, + cluster_id, + bucket_id: BASE64_STANDARD.encode(bucket), + scope, + payload, + }; + let response = self.capella_request(request, deadline, ctrl_c)?; + + if response.status() != 201 { + return Err(ClientError::RequestFailed { + reason: Some(response.content().into()), + key: None, + }); + } + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub fn delete_collection( + &self, + org_id: String, + project_id: String, + cluster_id: String, + bucket: String, + scope: String, + collection: String, + deadline: Instant, + ctrl_c: Arc, + ) -> Result<(), ClientError> { + let request = CapellaRequest::CollectionDelete { + org_id, + project_id, + cluster_id, + bucket_id: BASE64_STANDARD.encode(bucket), + scope, + collection, + }; + let response = self.capella_request(request, deadline, ctrl_c)?; + + if response.status() != 204 { + return Err(ClientError::RequestFailed { + reason: Some(response.content().into()), + key: None, + }); + } + + Ok(()) + } + + pub fn list_collections( + &self, + org_id: String, + project_id: String, + cluster_id: String, + bucket: String, + scope: String, + deadline: Instant, + ctrl_c: Arc, + ) -> Result { + let request = CapellaRequest::CollectionList { + org_id, + project_id, + cluster_id, + bucket_id: BASE64_STANDARD.encode(bucket), + scope, + }; + let response = self.capella_request(request, deadline, ctrl_c)?; + + if response.status() != 200 { + return Err(ClientError::RequestFailed { + reason: Some(response.content().into()), + key: None, + }); + } + + let resp: CollectionsResponse = serde_json::from_str(response.content())?; + Ok(resp) + } + pub fn create_scope( &self, org_id: String, project_id: String, cluster_id: String, bucket: String, - scope_name: String, + scope: String, deadline: Instant, ctrl_c: Arc, ) -> Result<(), ClientError> { @@ -611,7 +705,7 @@ impl CapellaClient { project_id, cluster_id, bucket_id: BASE64_STANDARD.encode(bucket), - payload: format!("{{\"name\": \"{}\"}}", scope_name), + payload: format!("{{\"name\": \"{}\"}}", scope), }; let response = self.capella_request(request, deadline, ctrl_c)?; @@ -631,7 +725,7 @@ impl CapellaClient { project_id: String, cluster_id: String, bucket: String, - scope_name: String, + scope: String, deadline: Instant, ctrl_c: Arc, ) -> Result<(), ClientError> { @@ -640,7 +734,7 @@ impl CapellaClient { project_id, cluster_id, bucket_id: BASE64_STANDARD.encode(bucket), - scope_name, + scope, }; let response = self.capella_request(request, deadline, ctrl_c)?; @@ -742,7 +836,7 @@ pub enum CapellaRequest { project_id: String, cluster_id: String, bucket_id: String, - scope_name: String, + scope: String, }, ScopeList { org_id: String, @@ -750,6 +844,29 @@ pub enum CapellaRequest { cluster_id: String, bucket_id: String, }, + CollectionCreate { + org_id: String, + project_id: String, + cluster_id: String, + bucket_id: String, + scope: String, + payload: String, + }, + CollectionDelete { + org_id: String, + project_id: String, + cluster_id: String, + bucket_id: String, + scope: String, + collection: String, + }, + CollectionList { + org_id: String, + project_id: String, + cluster_id: String, + bucket_id: String, + scope: String, + }, CredentialsCreate { org_id: String, project_id: String, @@ -899,11 +1016,11 @@ impl CapellaRequest { project_id, cluster_id, bucket_id, - scope_name, + scope, } => { format!( "/v4/organizations/{}/projects/{}/clusters/{}/buckets/{}/scopes/{}", - org_id, project_id, cluster_id, bucket_id, scope_name + org_id, project_id, cluster_id, bucket_id, scope ) } Self::ScopeList { @@ -914,7 +1031,45 @@ impl CapellaRequest { } => { format!( "/v4/organizations/{}/projects/{}/clusters/{}/buckets/{}/scopes", - org_id, project_id, cluster_id, bucket_id + org_id, project_id, cluster_id, bucket_id, + ) + } + Self::CollectionCreate { + org_id, + project_id, + cluster_id, + bucket_id, + scope, + .. + } => { + format!( + "/v4/organizations/{}/projects/{}/clusters/{}/buckets/{}/scopes/{}/collections", + org_id, project_id, cluster_id, bucket_id, scope + ) + } + Self::CollectionDelete { + org_id, + project_id, + cluster_id, + bucket_id, + scope, + collection, + } => { + format!( + "/v4/organizations/{}/projects/{}/clusters/{}/buckets/{}/scopes/{}/collections/{}", + org_id, project_id, cluster_id, bucket_id, scope, collection + ) + } + Self::CollectionList { + org_id, + project_id, + cluster_id, + bucket_id, + scope, + } => { + format!( + "/v4/organizations/{}/projects/{}/clusters/{}/buckets/{}/scopes/{}/collections", + org_id, project_id, cluster_id, bucket_id, scope ) } Self::CredentialsCreate { @@ -951,6 +1106,9 @@ impl CapellaRequest { Self::ScopeCreate { .. } => HttpVerb::Post, Self::ScopeDelete { .. } => HttpVerb::Delete, Self::ScopeList { .. } => HttpVerb::Get, + Self::CollectionCreate { .. } => HttpVerb::Post, + Self::CollectionDelete { .. } => HttpVerb::Delete, + Self::CollectionList { .. } => HttpVerb::Get, Self::CredentialsCreate { .. } => HttpVerb::Post, } } @@ -964,6 +1122,7 @@ impl CapellaRequest { Self::BucketUpdate { payload, .. } => Some(payload.as_bytes().into()), Self::AllowIPAddress { payload, .. } => Some(payload.as_bytes().into()), Self::ScopeCreate { payload, .. } => Some(payload.as_bytes().into()), + Self::CollectionCreate { payload, .. } => Some(payload.as_bytes().into()), Self::CredentialsCreate { payload, .. } => Some(payload.as_bytes().into()), _ => None, } diff --git a/src/client/cloud_json.rs b/src/client/cloud_json.rs index 2a7ff1cd..406c8bd6 100644 --- a/src/client/cloud_json.rs +++ b/src/client/cloud_json.rs @@ -403,6 +403,38 @@ impl Bucket { } } +#[derive(Debug, Deserialize)] +pub struct CollectionsResponse { + data: Vec, +} + +impl CollectionsResponse { + pub fn items(self) -> Vec { + self.data + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Collection { + name: String, + #[serde(rename = "maxTTL")] + max_expiry: i64, +} + +impl Collection { + pub fn new(name: String, max_expiry: i64) -> Collection { + Collection { name, max_expiry } + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn max_expiry(&self) -> i64 { + self.max_expiry + } +} + #[derive(Debug, Serialize)] pub(crate) struct CredentialsCreateRequest { name: String, diff --git a/tests/common/playground.rs b/tests/common/playground.rs index dee06c43..a7ebf56a 100644 --- a/tests/common/playground.rs +++ b/tests/common/playground.rs @@ -395,7 +395,11 @@ impl CBPlayground { Some(c) => c, }; Self::setup("wait_for_scope", Some(config), None, |dirs, sandbox| { - let cmd = r#"collections | select scope collection | to json"#; + let cmd = format!( + "collections --scope {} | select collection | to json", + scope_name + ) + .as_str(); sandbox.retry_until( Instant::now().add(Duration::from_secs(30)), Duration::from_millis(200), @@ -404,10 +408,8 @@ impl CBPlayground { RetryExpectations::ExpectOut, |json| -> TestResult { for item in json.as_array().unwrap() { - if item["scope"] == scope_name { - if item["collection"] == collection_name { - return Ok(true); - } + if item["collection"] == collection_name { + return Ok(true); } }