From 92c7422e5890fe965433aa9135f351b0c88e3b1e Mon Sep 17 00:00:00 2001 From: Jack Westwood Date: Thu, 3 Oct 2024 14:58:16 +0100 Subject: [PATCH] Support listing analytics clusters --- src/cli/analytics_clusters.rs | 143 ++++++++++++++++++++++++++++++++++ src/cli/clusters_get.rs | 13 +++- src/cli/mod.rs | 2 + src/client/cloud.rs | 47 +++++++---- src/client/cloud_json.rs | 60 ++++++++++++++ src/main.rs | 1 + 6 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 src/cli/analytics_clusters.rs diff --git a/src/cli/analytics_clusters.rs b/src/cli/analytics_clusters.rs new file mode 100644 index 00000000..dadfc8dd --- /dev/null +++ b/src/cli/analytics_clusters.rs @@ -0,0 +1,143 @@ +use crate::cli::client_error_to_shell_error; +use crate::cli::util::{convert_json_value_to_nu_value, find_org_id, find_project_id, NuValueMap}; +use crate::state::State; +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, +}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct AnalyticsClusters { + state: Arc>, +} + +impl AnalyticsClusters { + pub fn new(state: Arc>) -> Self { + Self { state } + } +} + +impl Command for AnalyticsClusters { + fn name(&self) -> &str { + "analytics clusters" + } + + fn signature(&self) -> Signature { + Signature::build("analytics clusters") + .named( + "organization", + SyntaxShape::String, + "the Capella organization to use", + None, + ) + .named( + "project", + SyntaxShape::String, + "the Capella project to use", + None, + ) + .switch("details", "return analytics clusters details", None) + .category(Category::Custom("couchbase".to_string())) + } + + fn usage(&self) -> &str { + "Lists all analytics clusters in the active Capella project" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + analytics_clusters(self.state.clone(), engine_state, stack, call, input) + } +} + +fn analytics_clusters( + state: Arc>, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let span = call.head; + let guard = state.lock().unwrap(); + let ctrl_c = engine_state.ctrlc.as_ref().unwrap().clone(); + + let control = + guard.named_or_active_org(call.get_flag(engine_state, stack, "organization")?)?; + + let project = + guard.named_or_active_project(call.get_flag(engine_state, stack, "project")?)?; + let client = control.client(); + + let org_id = find_org_id(ctrl_c.clone(), &client, span)?; + let project_id = find_project_id(ctrl_c.clone(), project, &client, span, org_id.clone())?; + + let analytics_clusters = client + .list_analytics_clusters(org_id, project_id, ctrl_c) + .map_err(|e| client_error_to_shell_error(e, span))?; + + let detail = call.has_flag(engine_state, stack, "details")?; + + let mut results = vec![]; + for cluster in analytics_clusters.items() { + let mut collected = NuValueMap::default(); + collected.add_string("name", cluster.name(), span); + collected.add_string("id", cluster.id(), span); + collected.add_string("state", cluster.state(), span); + collected.add_i64("number of nodes", cluster.nodes(), span); + collected.add( + "cloud provider", + convert_json_value_to_nu_value( + &serde_json::to_value(cluster.cloud_provider()).unwrap(), + span, + ) + .unwrap(), + ); + + if detail { + if !cluster.description().is_empty() { + collected.add_string("description", cluster.description(), span); + } + + collected.add( + "compute", + convert_json_value_to_nu_value( + &serde_json::to_value(cluster.compute()).unwrap(), + span, + ) + .unwrap(), + ); + collected.add( + "availability", + convert_json_value_to_nu_value( + &serde_json::to_value(cluster.availability()).unwrap(), + span, + ) + .unwrap(), + ); + collected.add( + "support", + convert_json_value_to_nu_value( + &serde_json::to_value(cluster.support()).unwrap(), + span, + ) + .unwrap(), + ); + } + + results.push(collected.into_value(span)) + } + + Ok(Value::List { + vals: results, + internal_span: span, + } + .into_pipeline_data()) +} diff --git a/src/cli/clusters_get.rs b/src/cli/clusters_get.rs index 5bba9f65..1b0a469b 100644 --- a/src/cli/clusters_get.rs +++ b/src/cli/clusters_get.rs @@ -4,10 +4,11 @@ use std::sync::{Arc, Mutex}; use crate::cli::error::client_error_to_shell_error; use crate::cli::util::{convert_json_value_to_nu_value, find_org_id, find_project_id, NuValueMap}; +use crate::client::cloud_json::Cluster; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Span, SyntaxShape}; #[derive(Clone)] pub struct ClustersGet { @@ -85,10 +86,16 @@ fn clusters_get( let project_id = find_project_id(ctrl_c.clone(), project, &client, span, org_id.clone())?; let cluster = client - .get_cluster(name, org_id.clone(), project_id.clone(), ctrl_c.clone()) + .get_cluster(name, org_id, project_id, ctrl_c) .map_err(|e| client_error_to_shell_error(e, span))?; let mut collected = NuValueMap::default(); + add_cluster_info(cluster, span, &mut collected); + + Ok(collected.into_pipeline_data(span)) +} + +fn add_cluster_info(cluster: Cluster, span: Span, collected: &mut NuValueMap) { collected.add_string("name", cluster.name(), span); collected.add_string("id", cluster.id(), span); collected.add_string("description", cluster.description(), span); @@ -144,6 +151,4 @@ fn clusters_get( if let Some(cmek_id) = cluster.cmek_id() { collected.add_string("cmek id", cmek_id, span); } - - Ok(collected.into_pipeline_data(span)) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f8961dcc..e4d4ee33 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,6 +1,7 @@ mod allow_ip; mod analytics; mod analytics_buckets; +mod analytics_clusters; mod analytics_datasets; mod analytics_dataverses; mod analytics_indexes; @@ -87,6 +88,7 @@ mod version; pub use allow_ip::AllowIP; pub use analytics::Analytics; pub use analytics_buckets::AnalyticsBuckets; +pub use analytics_clusters::AnalyticsClusters; pub use analytics_datasets::AnalyticsDatasets; pub use analytics_dataverses::AnalyticsDataverses; pub use analytics_indexes::AnalyticsIndexes; diff --git a/src/client/cloud.rs b/src/client/cloud.rs index c9db822f..1eb56bf2 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, Collection, CollectionsResponse, - OrganizationsResponse, ProjectsResponse, ScopesResponse, + AnalyticsClusterResponse, Bucket, BucketsResponse, Cluster, ClustersResponse, Collection, + CollectionsResponse, OrganizationsResponse, ProjectsResponse, ScopesResponse, }; use crate::client::error::ClientError; use crate::client::http_handler::{HttpResponse, HttpVerb}; @@ -258,17 +258,7 @@ impl CapellaClient { project_id: String, ctrl_c: Arc, ) -> Result { - let request = CapellaRequest::ClusterList { org_id, project_id }; - let response = self.capella_request(request, ctrl_c)?; - - if response.status() != 200 { - return Err(ClientError::RequestFailed { - reason: Some(response.content().into()), - key: None, - }); - } - - let resp: ClustersResponse = serde_json::from_str(response.content())?; + let resp = self.list_clusters(org_id, project_id, ctrl_c)?; for c in resp.items() { if c.name() == cluster_name { @@ -345,6 +335,26 @@ impl CapellaClient { Ok(()) } + pub fn list_analytics_clusters( + &self, + org_id: String, + project_id: String, + ctrl_c: Arc, + ) -> Result { + let request = CapellaRequest::AnalyticsClusterList { org_id, project_id }; + let response = self.capella_request(request, ctrl_c)?; + + if response.status() != 200 { + return Err(ClientError::RequestFailed { + reason: Some(response.content().into()), + key: None, + }); + } + + let resp: AnalyticsClusterResponse = serde_json::from_str(response.content())?; + Ok(resp) + } + pub fn create_credentials( &self, org_id: String, @@ -754,6 +764,10 @@ pub enum CapellaRequest { org_id: String, project_id: String, }, + AnalyticsClusterList { + org_id: String, + project_id: String, + }, BucketCreate { org_id: String, project_id: String, @@ -899,6 +913,12 @@ impl CapellaRequest { org_id, project_id ) } + Self::AnalyticsClusterList { org_id, project_id } => { + format!( + "/v4/organizations/{}/projects/{}/analyticsClusters", + org_id, project_id + ) + } Self::BucketCreate { org_id, project_id, @@ -1062,6 +1082,7 @@ impl CapellaRequest { Self::ClusterDelete { .. } => HttpVerb::Delete, Self::ClusterGet { .. } => HttpVerb::Get, Self::ClusterList { .. } => HttpVerb::Get, + Self::AnalyticsClusterList { .. } => HttpVerb::Get, Self::BucketCreate { .. } => HttpVerb::Post, Self::BucketDelete { .. } => HttpVerb::Delete, Self::BucketGet { .. } => HttpVerb::Get, diff --git a/src/client/cloud_json.rs b/src/client/cloud_json.rs index a373e1e1..b835ee0b 100644 --- a/src/client/cloud_json.rs +++ b/src/client/cloud_json.rs @@ -366,6 +366,66 @@ impl Cluster { } } +#[derive(Debug, Deserialize)] +pub(crate) struct AnalyticsClusterResponse { + data: Vec, +} + +impl AnalyticsClusterResponse { + pub fn items(&self) -> Vec { + self.data.clone() + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AnalyticsCluster { + id: String, + description: String, + name: String, + cloud_provider: String, + region: String, + nodes: i64, + current_state: String, + support: Support, + compute: Compute, + availability: Availability, +} + +impl AnalyticsCluster { + pub fn id(&self) -> String { + self.id.clone() + } + pub fn name(&self) -> String { + self.name.clone() + } + pub fn state(&self) -> String { + self.current_state.clone() + } + pub fn description(&self) -> String { + self.description.clone() + } + pub fn cloud_provider(&self) -> CloudProvider { + CloudProvider { + provider: self.cloud_provider.to_lowercase().clone(), + region: self.region.clone(), + cidr: None, + } + } + pub fn availability(&self) -> &Availability { + &self.availability + } + pub fn compute(&self) -> Compute { + self.compute.clone() + } + pub fn support(&self) -> &Support { + &self.support + } + pub fn nodes(&self) -> i64 { + self.nodes + } +} + #[derive(Debug, Deserialize)] pub(crate) struct BucketsResponse { data: Vec, diff --git a/src/main.rs b/src/main.rs index 138f1bde..662fdc7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -713,6 +713,7 @@ fn merge_couchbase_delta(context: &mut EngineState, state: Arc>) { working_set.add_decl(Box::new(AllowIP::new(state.clone()))); working_set.add_decl(Box::new(Analytics::new(state.clone()))); working_set.add_decl(Box::new(AnalyticsBuckets::new(state.clone()))); + working_set.add_decl(Box::new(AnalyticsClusters::new(state.clone()))); working_set.add_decl(Box::new(AnalyticsDatasets::new(state.clone()))); working_set.add_decl(Box::new(AnalyticsDataverses::new(state.clone()))); working_set.add_decl(Box::new(AnalyticsIndexes::new(state.clone())));