diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs index 659cd98bc3a..108b783938b 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs @@ -3,6 +3,7 @@ use ockam_core::Error; use crate::cli_state::CliState; use crate::cloud::space::Space; +use crate::cloud::subscription::Subscription; use super::Result; @@ -13,12 +14,14 @@ impl CliState { space_id: &str, space_name: &str, users: Vec<&str>, + subscription: Option<&Subscription>, ) -> Result { let repository = self.spaces_repository(); let space = Space { id: space_id.to_string(), name: space_name.to_string(), users: users.iter().map(|u| u.to_string()).collect(), + subscription: subscription.cloned(), }; repository.store_space(&space).await?; @@ -96,13 +99,26 @@ mod test { // the first created space becomes the default let space1 = cli - .store_space("1", "name1", vec!["me@ockam.io", "you@ockam.io"]) + .store_space( + "1", + "name1", + vec!["me@ockam.io", "you@ockam.io"], + Some(&Subscription { + name: "name1".to_string(), + is_free_trial: false, + marketplace: None, + start_date: None, + end_date: None, + }), + ) .await?; let result = cli.get_default_space().await?; assert_eq!(result, space1); // the store method can be used to update a space - let updated_space1 = cli.store_space("1", "name1", vec!["them@ockam.io"]).await?; + let updated_space1 = cli + .store_space("1", "name1", vec!["them@ockam.io"], None) + .await?; let result = cli.get_default_space().await?; assert_eq!(result, updated_space1); diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs index a1bd917b514..06633b8dd29 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs @@ -3,7 +3,7 @@ use sqlx::*; use ockam_core::async_trait; use ockam_core::Result; -use ockam_node::database::{Boolean, FromSqlxError, SqlxDatabase, ToVoid}; +use ockam_node::database::{Boolean, FromSqlxError, Nullable, SqlxDatabase, ToVoid}; use crate::cloud::space::Space; @@ -42,14 +42,20 @@ impl SpacesRepository for SpacesSqlxDatabase { let query2 = query( r#" - INSERT INTO space (space_id, space_name, is_default) - VALUES ($1, $2, $3) + INSERT INTO space (space_id, space_name, is_default, subscription) + VALUES ($1, $2, $3, $4) ON CONFLICT (space_id) - DO UPDATE SET space_name = $2, is_default = $3"#, + DO UPDATE SET space_name = $2, is_default = $3, subscription = $4"#, ) .bind(&space.id) .bind(&space.name) - .bind(is_already_default); + .bind(is_already_default) + .bind( + space + .subscription + .as_ref() + .and_then(|s| serde_json::to_string(s).ok()), + ); query2.execute(&mut *transaction).await.void()?; // remove any existing users related to that space if any @@ -91,7 +97,8 @@ impl SpacesRepository for SpacesSqlxDatabase { let mut transaction = self.database.begin().await.into_core()?; let query1 = - query_as("SELECT space_id, space_name FROM space WHERE space_name = $1").bind(name); + query_as("SELECT space_id, space_name, subscription FROM space WHERE space_name = $1") + .bind(name); let row: Option = query1.fetch_optional(&mut *transaction).await.into_core()?; let space = match row.map(|r| r.space()) { Some(mut space) => { @@ -113,7 +120,7 @@ impl SpacesRepository for SpacesSqlxDatabase { async fn get_spaces(&self) -> Result> { let mut transaction = self.database.begin().await.into_core()?; - let query = query_as("SELECT space_id, space_name FROM space"); + let query = query_as("SELECT space_id, space_name, subscription FROM space"); let row: Vec = query.fetch_all(&mut *transaction).await.into_core()?; let mut spaces = vec![]; @@ -180,6 +187,7 @@ impl SpacesRepository for SpacesSqlxDatabase { struct SpaceRow { space_id: String, space_name: String, + subscription: Nullable, } impl SpaceRow { @@ -192,6 +200,11 @@ impl SpaceRow { id: self.space_id.clone(), name: self.space_name.clone(), users: user_emails, + subscription: self + .subscription + .to_option() + .as_ref() + .and_then(|s| serde_json::from_str(s).ok()), } } } @@ -207,6 +220,7 @@ struct UserSpaceRow { #[cfg(test)] mod test { use super::*; + use crate::cloud::subscription::Subscription; use ockam_node::database::with_dbs; use std::sync::Arc; @@ -220,6 +234,7 @@ mod test { id: "1".to_string(), name: "name1".to_string(), users: vec!["me@ockam.io".to_string(), "you@ockam.io".to_string()], + subscription: None, }; let mut space2 = Space { id: "2".to_string(), @@ -229,6 +244,13 @@ mod test { "him@ockam.io".to_string(), "her@ockam.io".to_string(), ], + subscription: Some(Subscription { + name: "premium".to_string(), + is_free_trial: false, + marketplace: Some("aws".to_string()), + start_date: Some(chrono::Utc::now().to_string()), + end_date: Some((chrono::Utc::now() + chrono::Duration::days(30)).to_string()), + }), }; repository.store_space(&space1).await?; diff --git a/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs b/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs index a759143996f..936ab810262 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs @@ -1,14 +1,13 @@ use crate::address::extract_address_value; use crate::cli_state::EnrollmentTicket; use crate::cloud::email_address::EmailAddress; +use crate::date::is_expired; use crate::error::ApiError; use crate::output::Output; use minicbor::{CborLen, Decode, Encode}; use ockam::identity::Identifier; use serde::{Deserialize, Serialize}; use std::{fmt::Display, str::FromStr}; -use time::format_description::well_known::iso8601::Iso8601; -use time::OffsetDateTime; #[derive(Clone, Debug, Eq, PartialEq, Decode, Encode, CborLen, Deserialize, Serialize)] #[cbor(index_only)] @@ -149,20 +148,6 @@ impl Output for SentInvitation { } } -/// Check if a string that represents an Iso8601 date is expired, using the `time` crate -fn is_expired(date: &str) -> ockam_core::Result { - // Add the Z timezone to the date, as the `time` crate requires it - let date = if date.ends_with('Z') { - date.to_string() - } else { - format!("{}Z", date) - }; - let now = OffsetDateTime::now_utc(); - let date = OffsetDateTime::parse(&date, &Iso8601::DEFAULT) - .map_err(|e| ApiError::core(e.to_string()))?; - Ok(date < now) -} - #[derive(Clone, Debug, Encode, Decode, CborLen, Deserialize, Serialize, PartialEq)] #[cbor(map)] #[rustfmt::skip] @@ -192,7 +177,9 @@ impl ServiceAccessDetails { #[cfg(test)] mod test { - use super::*; + use crate::date::is_expired; + use time::format_description::well_known::Iso8601; + use time::OffsetDateTime; #[test] fn test_is_expired() { diff --git a/implementations/rust/ockam/ockam_api/src/cloud/space.rs b/implementations/rust/ockam/ockam_api/src/cloud/space.rs index 28d32677773..f334f13c345 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/space.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/space.rs @@ -1,18 +1,19 @@ -use colorful::Colorful; use miette::IntoDiagnostic; use minicbor::{CborLen, Decode, Encode}; use serde::Serialize; -use std::fmt::Write; +use std::fmt::{Display, Formatter}; use ockam_core::api::Request; use ockam_core::async_trait; use ockam_node::Context; use crate::cloud::project::{Project, ProjectsOrchestratorApi}; +use crate::cloud::subscription::Subscription; use crate::cloud::{ControllerClient, HasSecureClient}; -use crate::colors::OckamColor; +use crate::colors::color_primary; use crate::nodes::InMemoryNode; use crate::output::{comma_separated, Output}; +use crate::terminal::fmt; const TARGET: &str = "ockam_api::cloud::space"; @@ -23,6 +24,7 @@ pub struct Space { #[n(1)] pub id: String, #[n(2)] pub name: String, #[n(3)] pub users: Vec, + #[n(4)] pub subscription: Option, } impl Space { @@ -33,37 +35,41 @@ impl Space { pub fn space_name(&self) -> String { self.name.clone() } -} -impl Output for Space { - fn item(&self) -> crate::Result { - let mut w = String::new(); - write!(w, "Space")?; - write!(w, "\n Id: {}", self.id)?; - write!(w, "\n Name: {}", self.name)?; - write!(w, "\n Users: {}", comma_separated(&self.users))?; - Ok(w) + pub fn has_subscription(&self) -> bool { + self.subscription.is_some() } - fn as_list_item(&self) -> crate::Result { - let mut output = String::new(); - writeln!( - output, - "Space {}", - self.name - .to_string() - .color(OckamColor::PrimaryResource.color()) - )?; + pub fn is_in_free_trial_subscription(&self) -> bool { + self.subscription.is_none() + || self + .subscription + .as_ref() + .map(|s| s.is_free_trial) + .unwrap_or_default() + } +} + +impl Display for Space { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", color_primary(&self.name))?; + writeln!(f, "{}Id: {}", fmt::INDENTATION, color_primary(&self.id))?; writeln!( - output, - "Id {}", - self.id - .to_string() - .color(OckamColor::PrimaryResource.color()) + f, + "{}Users: {}", + fmt::INDENTATION, + comma_separated(&self.users) )?; - write!(output, "{}", comma_separated(&self.users))?; + if let Some(subscription) = &self.subscription { + write!(f, "{}", subscription.iter_output()?.indent())?; + } + Ok(()) + } +} - Ok(output) +impl Output for Space { + fn item(&self) -> crate::Result { + Ok(self.padded_display()?) } } @@ -118,6 +124,7 @@ impl Spaces for InMemoryNode { &space.id, &space.name, space.users.iter().map(|u| u.as_ref()).collect(), + space.subscription.as_ref(), ) .await?; Ok(space) @@ -132,6 +139,7 @@ impl Spaces for InMemoryNode { &space.id, &space.name, space.users.iter().map(|u| u.as_ref()).collect(), + space.subscription.as_ref(), ) .await?; Ok(space) @@ -189,6 +197,7 @@ impl Spaces for InMemoryNode { &space.id, &space.name, space.users.iter().map(|u| u.as_ref()).collect(), + space.subscription.as_ref(), ) .await?; @@ -281,6 +290,7 @@ pub mod tests { id: String::arbitrary(g), name: String::arbitrary(g), users: vec![String::arbitrary(g), String::arbitrary(g)], + subscription: bool::arbitrary(g).then(|| Subscription::arbitrary(g)), } } } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs b/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs index 4c7618e30a3..22df122bdf0 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs @@ -1,10 +1,13 @@ -use minicbor::{CborLen, Decode, Encode}; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; - use crate::cloud::{ControllerClient, HasSecureClient}; use crate::output::Output; +use minicbor::{CborLen, Decode, Encode}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter, Write}; +use time::OffsetDateTime; +use crate::colors::color_primary; +use crate::date::parse_date; +use crate::terminal::fmt; use ockam_core::api::{Error, Reply, Request, Status}; use ockam_core::{self, async_trait, Result}; use ockam_node::Context; @@ -50,14 +53,80 @@ impl ActivateSubscription { } } -#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Debug)] -#[cfg_attr(test, derive(Clone))] +#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[cbor(map)] pub struct Subscription { + #[n(1)] + pub name: String, + #[n(2)] + pub is_free_trial: bool, + #[n(3)] + pub marketplace: Option, + #[n(4)] + pub start_date: Option, + #[n(5)] + pub end_date: Option, +} + +impl Subscription { + pub fn end_date(&self) -> Option { + self.end_date + .as_ref() + .and_then(|date| parse_date(date).ok()) + } + + pub fn start_date(&self) -> Option { + self.start_date + .as_ref() + .and_then(|date| parse_date(date).ok()) + } +} + +impl Display for Subscription { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Subscription: {}", color_primary(&self.name))?; + if self.is_free_trial { + writeln!(f, " (free trial)")?; + } else { + writeln!(f)?; + } + + if let (Some(start_date), Some(end_date)) = (self.start_date(), self.end_date()) { + writeln!( + f, + "{}Started at {}, expires at {}", + fmt::INDENTATION, + color_primary(start_date.to_string()), + color_primary(end_date.to_string()), + )?; + } + + if let Some(marketplace) = &self.marketplace { + writeln!( + f, + "{}Marketplace: {}", + fmt::INDENTATION, + color_primary(marketplace) + )?; + } + + Ok(()) + } +} + +impl Output for Subscription { + fn item(&self) -> crate::Result { + Ok(self.padded_display()?) + } +} + +#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cbor(map)] +pub struct SubscriptionLegacy { #[n(1)] pub id: String, #[n(2)] - marketplace: String, + pub marketplace: String, #[n(3)] pub status: String, #[n(4)] @@ -70,20 +139,20 @@ pub struct Subscription { pub space_id: Option, } -impl Output for Subscription { +impl Output for SubscriptionLegacy { fn item(&self) -> crate::Result { let mut w = String::new(); - write!(w, "Subscription")?; - write!(w, "\n Id: {}", self.id)?; - write!(w, "\n Status: {}", self.status)?; + write!(w, "{}Id: {}", fmt::PADDING, self.id)?; + write!(w, "{}Status: {}", fmt::PADDING, self.status)?; write!( w, - "\n Space id: {}", + "{}Space id: {}", + fmt::PADDING, self.space_id.clone().unwrap_or("N/A".to_string()) )?; - write!(w, "\n Entitlements: {}", self.entitlements)?; - write!(w, "\n Metadata: {}", self.metadata)?; - write!(w, "\n Contact info: {}", self.contact_info)?; + write!(w, "{}Entitlements: {}", fmt::PADDING, self.entitlements)?; + write!(w, "{}Metadata: {}", fmt::PADDING, self.metadata)?; + write!(w, "{}Contact info: {}", fmt::PADDING, self.contact_info)?; Ok(w) } } @@ -95,41 +164,41 @@ pub trait Subscriptions { ctx: &Context, space_id: String, subscription_data: String, - ) -> Result>; + ) -> Result>; async fn unsubscribe( &self, ctx: &Context, subscription_id: String, - ) -> Result>; + ) -> Result>; async fn update_subscription_contact_info( &self, ctx: &Context, subscription_id: String, contact_info: String, - ) -> Result>; + ) -> Result>; async fn update_subscription_space( &self, ctx: &Context, subscription_id: String, new_space_id: String, - ) -> Result>; + ) -> Result>; - async fn get_subscriptions(&self, ctx: &Context) -> Result>>; + async fn get_subscriptions(&self, ctx: &Context) -> Result>>; async fn get_subscription( &self, ctx: &Context, subscription_id: String, - ) -> Result>; + ) -> Result>; async fn get_subscription_by_space_id( &self, ctx: &Context, space_id: String, - ) -> Result>; + ) -> Result>; } #[async_trait] @@ -140,7 +209,7 @@ impl Subscriptions for ControllerClient { ctx: &Context, space_id: String, subscription_data: String, - ) -> Result> { + ) -> Result> { let req_body = ActivateSubscription::existing(space_id, subscription_data); trace!(target: TARGET, space_id = ?req_body.space_id, space_name = ?req_body.space_name, "activating subscription"); let req = Request::post("/v0/activate").body(req_body); @@ -152,7 +221,7 @@ impl Subscriptions for ControllerClient { &self, ctx: &Context, subscription_id: String, - ) -> Result> { + ) -> Result> { trace!(target: TARGET, subscription = %subscription_id, "unsubscribing"); let req = Request::put(format!("/v0/{subscription_id}/unsubscribe")); self.get_secure_client().ask(ctx, API_SERVICE, req).await @@ -164,7 +233,7 @@ impl Subscriptions for ControllerClient { ctx: &Context, subscription_id: String, contact_info: String, - ) -> Result> { + ) -> Result> { trace!(target: TARGET, subscription = %subscription_id, "updating subscription contact info"); let req = Request::put(format!("/v0/{subscription_id}/contact_info")).body(contact_info); self.get_secure_client().ask(ctx, API_SERVICE, req).await @@ -176,14 +245,14 @@ impl Subscriptions for ControllerClient { ctx: &Context, subscription_id: String, new_space_id: String, - ) -> Result> { + ) -> Result> { trace!(target: TARGET, subscription = %subscription_id, new_space_id = %new_space_id, "updating subscription space"); let req = Request::put(format!("/v0/{subscription_id}/space_id")).body(new_space_id); self.get_secure_client().ask(ctx, API_SERVICE, req).await } #[instrument(skip_all)] - async fn get_subscriptions(&self, ctx: &Context) -> Result>> { + async fn get_subscriptions(&self, ctx: &Context) -> Result>> { trace!(target: TARGET, "listing subscriptions"); let req = Request::get("/v0/"); self.get_secure_client().ask(ctx, API_SERVICE, req).await @@ -194,7 +263,7 @@ impl Subscriptions for ControllerClient { &self, ctx: &Context, subscription_id: String, - ) -> Result> { + ) -> Result> { trace!(target: TARGET, subscription = %subscription_id, "getting subscription"); let req = Request::get(format!("/v0/{subscription_id}")); self.get_secure_client().ask(ctx, API_SERVICE, req).await @@ -205,8 +274,9 @@ impl Subscriptions for ControllerClient { &self, ctx: &Context, space_id: String, - ) -> Result> { - let subscriptions: Vec = self.get_subscriptions(ctx).await?.success()?; + ) -> Result> { + let subscriptions: Vec = + self.get_subscriptions(ctx).await?.success()?; let subscription = subscriptions .into_iter() .find(|s| s.space_id == Some(space_id.clone())); @@ -222,14 +292,12 @@ impl Subscriptions for ControllerClient { #[cfg(test)] pub mod tests { - use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; - - use crate::schema::tests::validate_with_schema; - use super::*; + use crate::schema::tests::validate_with_schema; + use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; quickcheck! { - fn subcription(s: Subscription) -> TestResult { + fn subcription_legacy(s: SubscriptionLegacy) -> TestResult { validate_with_schema("subscription", s) } @@ -238,9 +306,9 @@ pub mod tests { } } - impl Arbitrary for Subscription { + impl Arbitrary for SubscriptionLegacy { fn arbitrary(g: &mut Gen) -> Self { - Subscription { + SubscriptionLegacy { id: String::arbitrary(g), marketplace: String::arbitrary(g), status: String::arbitrary(g), @@ -252,6 +320,18 @@ pub mod tests { } } + impl Arbitrary for Subscription { + fn arbitrary(g: &mut Gen) -> Self { + Subscription { + name: String::arbitrary(g), + is_free_trial: bool::arbitrary(g), + marketplace: Option::arbitrary(g), + start_date: Option::arbitrary(g), + end_date: Option::arbitrary(g), + } + } + } + impl Arbitrary for ActivateSubscription { fn arbitrary(g: &mut Gen) -> Self { ActivateSubscription::create( diff --git a/implementations/rust/ockam/ockam_api/src/date.rs b/implementations/rust/ockam/ockam_api/src/date.rs new file mode 100644 index 00000000000..81d36dfc984 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/date.rs @@ -0,0 +1,21 @@ +use crate::ApiError; +use time::format_description::well_known::Iso8601; +use time::OffsetDateTime; + +/// Transform a string that represents an Iso8601 date into a `time::OffsetDateTime` +pub fn parse_date(date: &str) -> ockam_core::Result { + // Add the Z timezone to the date, as the `time` crate requires it + let date = if date.ends_with('Z') { + date.to_string() + } else { + format!("{}Z", date) + }; + OffsetDateTime::parse(&date, &Iso8601::DEFAULT).map_err(|e| ApiError::core(e.to_string())) +} + +/// Check if a string that represents an Iso8601 date is expired, using the `time` crate +pub fn is_expired(date: &str) -> ockam_core::Result { + let date = parse_date(date)?; + let now = OffsetDateTime::now_utc(); + Ok(date < now) +} diff --git a/implementations/rust/ockam/ockam_api/src/lib.rs b/implementations/rust/ockam/ockam_api/src/lib.rs index e5fe3f05d50..0d1f216f0b6 100644 --- a/implementations/rust/ockam/ockam_api/src/lib.rs +++ b/implementations/rust/ockam/ockam_api/src/lib.rs @@ -43,6 +43,7 @@ pub mod logs; mod schema; mod session; +mod date; mod rendezvous_healthcheck; pub mod test_utils; mod ui; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs index 68de26633c8..f5d04c4b0ca 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs @@ -241,10 +241,4 @@ impl Output for ServiceStatus { writeln!(f, "{}{}", fmt::PADDING, self)?; Ok(f) } - - fn as_list_item(&self) -> crate::Result { - let mut f = String::new(); - writeln!(f, "{}", self)?; - Ok(f) - } } diff --git a/implementations/rust/ockam/ockam_api/src/schema/schema.cddl b/implementations/rust/ockam/ockam_api/src/schema/schema.cddl index 4a8013a4dd9..c4339e35f90 100644 --- a/implementations/rust/ockam/ockam_api/src/schema/schema.cddl +++ b/implementations/rust/ockam/ockam_api/src/schema/schema.cddl @@ -19,14 +19,12 @@ space = { 1: space_id 2: space_name 3: [+ user] + ?4: subscription } user = text - spaces = [* space] - - create_space = { ?0: 3888657, 1: space_name @@ -237,6 +235,14 @@ onetime_code = { ;;; Subscription ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; subscription = { + 1: text, ;; name + 2: bool, ;; is_free_trial + ?3: text, ;; marketplace + ?4: text, ;; start_date + ?5: text, ;; end_date +} + +subscription_legacy = { 1: text, ;; id 2: text, ;; marketplace 3: text, ;; status diff --git a/implementations/rust/ockam/ockam_api/src/ui/output/mod.rs b/implementations/rust/ockam/ockam_api/src/ui/output/mod.rs index ac8b389ab19..6fe492775d8 100644 --- a/implementations/rust/ockam/ockam_api/src/ui/output/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/ui/output/mod.rs @@ -9,8 +9,10 @@ pub use utils::*; use crate::Result; +use crate::terminal::fmt; +use itertools::Itertools; use ockam_core::api::Reply; -use std::fmt::Write; +use std::fmt::{Display, Error, Write}; /// Trait to control how a given type will be printed in the UI layer. /// @@ -40,7 +42,26 @@ pub trait Output { fn item(&self) -> Result; fn as_list_item(&self) -> Result { - self.item() + Ok(self + .item()? + .lines() + .map(|l| l.strip_prefix(fmt::PADDING).unwrap_or(l)) + .join("\n")) + } + + fn padded_display(&self) -> std::result::Result + where + Self: Display, + { + Ok(self.iter_output()?.pad().to_string()) + } + + /// Return an iterator over the lines of the Display output + fn iter_output(&self) -> std::result::Result + where + Self: Display, + { + Ok(OutputIter::new(self.to_string())) } } @@ -81,3 +102,39 @@ impl Output for Reply { } } } + +pub struct OutputIter { + contents: String, +} + +impl Display for OutputIter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.contents) + } +} + +impl OutputIter { + pub fn new(contents: String) -> Self { + Self { contents } + } + + pub fn pad(self) -> Self { + let contents = self + .contents + .lines() + .map(|s| format!("{}{}", fmt::PADDING, s)) + .collect::>() + .join("\n"); + Self { contents } + } + + pub fn indent(self) -> Self { + let contents = self + .contents + .lines() + .map(|s| format!("{}{}", fmt::INDENTATION, s)) + .collect::>() + .join("\n"); + Self { contents } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs b/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs index 348d62f1900..053783dcf8d 100644 --- a/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs +++ b/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs @@ -5,10 +5,14 @@ use colorful::Colorful; use ockam::identity::TimestampInSeconds; pub fn comma_separated>(data: &[T]) -> String { - data.iter() - .map(AsRef::as_ref) - .collect::>() - .join(", ") + if data.is_empty() { + "-".to_string() + } else { + data.iter() + .map(AsRef::as_ref) + .collect::>() + .join(", ") + } } pub fn human_readable_time(time: TimestampInSeconds) -> String { diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index 41de5a86099..7e13d6ddeed 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -14,6 +14,12 @@ use tokio::sync::Mutex; use tokio::try_join; use tracing::{error, info, instrument, warn}; +use crate::enroll::OidcServiceExt; +use crate::error::Error; +use crate::operation::util::check_for_project_completion; +use crate::project::util::check_project_readiness; +use crate::util::async_cmd; +use crate::{docs, CommandGlobalOpts, Result}; use ockam::Context; use ockam_api::cli_state::journeys::{JourneyEvent, USER_EMAIL, USER_NAME}; use ockam_api::cli_state::random_name; @@ -30,13 +36,6 @@ use ockam_api::terminal::notification::NotificationHandler; use ockam_api::{fmt_log, fmt_ok, fmt_warn}; use ockam_api::{fmt_separator, CliState}; -use crate::enroll::OidcServiceExt; -use crate::error::Error; -use crate::operation::util::check_for_project_completion; -use crate::project::util::check_project_readiness; -use crate::util::async_cmd; -use crate::{docs, CommandGlobalOpts, Result}; - const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); @@ -417,7 +416,7 @@ async fn get_user_space( let progress_output = opts.terminal.loop_messages(&message, &is_finished); let (spaces, _) = try_join!(get_spaces, progress_output)?; - + let mut is_new = false; // If the identity has no spaces, create one let space = match spaces.first() { None => { @@ -449,6 +448,7 @@ async fn get_user_space( "Created a new Space named {}.", color_primary(space.name.clone()) ))?; + is_new = true; space } Some(space) => { @@ -463,12 +463,32 @@ async fn get_user_space( "Marked {} as your default Space, on this machine.\n", color_primary(space.name.clone()) ))?; - - opts.terminal.write_line(fmt_log!("This Space does not have a Subscription attached to it."))? - .write_line(fmt_log!("As a courtesy, we created a temporary Space for you, so you can continue to build.\n"))? - .write_line(fmt_log!("Please subscribe to an Ockam plan within two weeks {}", color_uri("https://www.ockam.io/pricing")))? - .write_line(fmt_log!("{}\n", "If you don't subscribe in that time, your Space and all Projects will be permanently deleted.".color(OckamColor::FmtWARNBackground.color())))?; - + if let Some(subscription) = &space.subscription { + opts.terminal.write_line(fmt_log!( + "This Space has a {} Subscription attached to it.", + color_primary(&subscription.name) + ))?; + if subscription.is_free_trial { + if is_new { + opts.terminal.write_line("")? + .write_line(fmt_log!("As a courtesy, we created a temporary Space for you, so you can continue to build.\n"))? + .write_line(fmt_log!("Please subscribe to an Ockam plan within two weeks {}", color_uri("https://www.ockam.io/pricing")))? + .write_line(fmt_log!("{}", "If you don't subscribe in that time, your Space and all Projects will be permanently deleted.".color(OckamColor::FmtWARNBackground.color())))?; + } else if let (Some(start_date), Some(end_date)) = + (&subscription.start_date, &subscription.end_date) + { + opts.terminal.write_line("")? + .write_line(fmt_log!("Your free trial started on {} and will end on {}.\n", start_date, end_date))? + .write_line(fmt_log!("Please subscribe to an Ockam plan before the trial ends to avoid any service interruptions {}", color_uri("https://www.ockam.io/pricing")))? + .write_line(fmt_log!("{}", "If you don't subscribe in that time, your Space and all Projects will be permanently deleted.".color(OckamColor::FmtWARNBackground.color())))?; + } + } + } else { + opts.terminal.write_line(fmt_log!( + "This Space does not have a Subscription attached to it." + ))?; + } + opts.terminal.write_line("")?; Ok(Some(space)) } diff --git a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs index 0c15469d69a..138f8f0d9aa 100644 --- a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs @@ -191,30 +191,32 @@ impl MemberOutput { attributes, } } +} - fn to_string(&self, padding: &str) -> ockam_api::Result { +impl Output for MemberOutput { + fn item(&self) -> ockam_api::Result { let mut f = String::new(); writeln!( f, "{}{}", - padding, + fmt::PADDING, color_primary(self.identifier.to_string()) )?; if self.attributes.attrs().is_empty() { - writeln!(f, "{}Has no attributes", padding)?; + writeln!(f, "{}Has no attributes", fmt::PADDING)?; } else { let attributes = self.attributes.deserialized_key_value_attrs(); writeln!( f, "{}With attributes: {}", - padding, + fmt::PADDING, color_primary(attributes.join(", ")) )?; writeln!( f, "{}{}Added at: {}", - padding, + fmt::PADDING, fmt::INDENTATION, color_warn(self.attributes.added_at().to_string()) )?; @@ -222,7 +224,7 @@ impl MemberOutput { writeln!( f, "{}{}Expires at: {}", - padding, + fmt::PADDING, fmt::INDENTATION, color_warn(expires_at.to_string()) )?; @@ -231,7 +233,7 @@ impl MemberOutput { writeln!( f, "{}{}Attested by: {}", - padding, + fmt::PADDING, fmt::INDENTATION, color_primary(attested_by.to_string()) )?; @@ -240,13 +242,3 @@ impl MemberOutput { Ok(f) } } - -impl Output for MemberOutput { - fn item(&self) -> ockam_api::Result { - self.to_string(fmt::PADDING) - } - - fn as_list_item(&self) -> ockam_api::Result { - self.to_string("") - } -} diff --git a/implementations/rust/ockam/ockam_command/src/space/create.rs b/implementations/rust/ockam/ockam_command/src/space/create.rs index 35d4044d773..18b8ebd5970 100644 --- a/implementations/rust/ockam/ockam_command/src/space/create.rs +++ b/implementations/rust/ockam/ockam_command/src/space/create.rs @@ -1,16 +1,17 @@ +use async_trait::async_trait; use clap::Args; -use colorful::Colorful; use miette::miette; use ockam::Context; use ockam_api::cli_state::random_name; use ockam_api::cloud::space::Spaces; +use ockam_api::colors::{color_uri, color_warn}; +use ockam_api::fmt_log; use ockam_api::nodes::InMemoryNode; use crate::shared_args::IdentityOpts; -use crate::util::async_cmd; use crate::util::validators::cloud_resource_name_validator; -use crate::{docs, CommandGlobalOpts}; +use crate::{docs, Command, CommandGlobalOpts, Result}; use ockam_api::output::Output; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -35,61 +36,55 @@ pub struct CreateCommand { pub identity_opts: IdentityOpts, } -impl CreateCommand { - pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { - async_cmd(&self.name(), opts.clone(), |ctx| async move { - self.async_run(&ctx, opts).await - }) - } - - pub fn name(&self) -> String { - "space crate".into() - } +#[async_trait] +impl Command for CreateCommand { + const NAME: &'static str = "space create"; - async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> Result<()> { if !opts .state .is_identity_enrolled(&self.identity_opts.identity_name) .await? { - return Err(miette!( - "Please enroll using 'ockam enroll' before using this command" - )); + return Err( + miette!("Please enroll using 'ockam enroll' before using this command").into(), + ); }; - opts.terminal.write_line(format!( - "\n{}", - "Creating a trial space for you (everything in it will be deleted in 15 days) ..." - .light_magenta(), - ))?; - opts.terminal.write_line(format!( - "{}", - "To learn more about production ready spaces in Ockam Orchestrator, contact us at: hello@ockam.io".light_magenta() - ))?; - let node = InMemoryNode::start(ctx, &opts.state).await?; - let space = node - .create_space( + + let space = { + let pb = opts.terminal.progress_bar(); + if let Some(pb) = pb.as_ref() { + pb.set_message("Creating a Space for you..."); + } + node.create_space( ctx, &self.name, self.admins.iter().map(|a| a.as_ref()).collect(), ) - .await?; - + .await? + }; + if space.is_in_free_trial_subscription() { + opts.terminal.write_line(fmt_log!("This Space does not have a Subscription attached to it."))? + .write_line(fmt_log!("As a courtesy, we created a temporary Space for you, so you can continue to build.\n"))? + .write_line(fmt_log!("Please subscribe to an Ockam plan within two weeks {}", color_uri("https://www.ockam.io/pricing")))? + .write_line(fmt_log!("{}\n", color_warn("If you don't subscribe in that time, your Space and all Projects will be permanently deleted.")))?; + } opts.terminal .stdout() .plain(space.item()?) - .json(serde_json::json!(&space)) + .json_obj(&space)? .write_line()?; Ok(()) } } -fn validate_space_name(s: &str) -> Result { +fn validate_space_name(s: &str) -> std::result::Result { match cloud_resource_name_validator(s) { Ok(_) => Ok(s.to_string()), Err(_e) => Err(String::from( - "space name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ + "The Space name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ Separators must occur between alphanumeric characters. This implies that separators can't \ occur at the start or end of the name, nor they can occur in sequence.", )) diff --git a/implementations/rust/ockam/ockam_command/src/space/delete.rs b/implementations/rust/ockam/ockam_command/src/space/delete.rs index 8a8f52b2ea5..ce179d149bf 100644 --- a/implementations/rust/ockam/ockam_command/src/space/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/space/delete.rs @@ -1,21 +1,19 @@ +use async_trait::async_trait; use clap::Args; use colorful::Colorful; use console::Term; -use miette::IntoDiagnostic; -use crate::{docs, CommandGlobalOpts}; +use crate::{docs, Command, CommandGlobalOpts}; use ockam::Context; use ockam_api::cloud::space::Spaces; use ockam_api::colors::OckamColor; use ockam_api::nodes::InMemoryNode; use ockam_api::terminal::{Terminal, TerminalStream}; use ockam_api::{color, fmt_ok}; -use ockam_core::AsyncTryClone; use crate::shared_args::IdentityOpts; use crate::terminal::tui::DeleteCommandTui; use crate::tui::PluralTerm; -use crate::util::async_cmd; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -39,41 +37,29 @@ pub struct DeleteCommand { yes: bool, } -impl DeleteCommand { - pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { - async_cmd(&self.name(), opts.clone(), |ctx| async move { - self.async_run(&ctx, opts).await - }) - } - - pub fn name(&self) -> String { - "space delete".into() - } +#[async_trait] +impl Command for DeleteCommand { + const NAME: &'static str = "space delete"; - async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { - DeleteTui::run( - ctx.async_try_clone().await.into_diagnostic()?, - opts, - self.clone(), - ) - .await + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + Ok(DeleteTui::run(ctx, opts, self).await?) } } -pub struct DeleteTui { - ctx: Context, +pub struct DeleteTui<'a> { + ctx: &'a Context, opts: CommandGlobalOpts, node: InMemoryNode, cmd: DeleteCommand, } -impl DeleteTui { +impl<'a> DeleteTui<'a> { pub async fn run( - ctx: Context, + ctx: &'a Context, opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let node = InMemoryNode::start(&ctx, &opts.state).await?; + let node = InMemoryNode::start(ctx, &opts.state).await?; let tui = Self { ctx, opts, @@ -85,7 +71,7 @@ impl DeleteTui { } #[ockam_core::async_trait] -impl DeleteCommandTui for DeleteTui { +impl<'a> DeleteCommandTui for DeleteTui<'a> { const ITEM_NAME: PluralTerm = PluralTerm::Space; fn cmd_arg_item_name(&self) -> Option { @@ -116,7 +102,7 @@ impl DeleteCommandTui for DeleteTui { } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { - self.node.delete_space_by_name(&self.ctx, item_name).await?; + self.node.delete_space_by_name(self.ctx, item_name).await?; self.terminal() .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/space/list.rs b/implementations/rust/ockam/ockam_command/src/space/list.rs index 2d8a3ef449c..f9a59269b34 100644 --- a/implementations/rust/ockam/ockam_command/src/space/list.rs +++ b/implementations/rust/ockam/ockam_command/src/space/list.rs @@ -1,16 +1,12 @@ +use async_trait::async_trait; use clap::Args; -use miette::IntoDiagnostic; -use opentelemetry::trace::FutureExt; -use tokio::sync::Mutex; -use tokio::try_join; use ockam::Context; use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; use crate::shared_args::IdentityOpts; -use crate::util::async_cmd; -use crate::{docs, CommandGlobalOpts}; +use crate::{docs, Command, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -28,44 +24,30 @@ pub struct ListCommand { pub identity_opts: IdentityOpts, } -impl ListCommand { - pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { - async_cmd(&self.name(), opts.clone(), |ctx| async move { - self.async_run(&ctx, opts).await - }) - } - - pub fn name(&self) -> String { - "space list".into() - } +#[async_trait] +impl Command for ListCommand { + const NAME: &'static str = "space list"; - async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { - let is_finished: Mutex = Mutex::new(false); + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { let node = InMemoryNode::start(ctx, &opts.state).await?; - let get_spaces = async { - let spaces = node.get_spaces(ctx).await?; - *is_finished.lock().await = true; - Ok(spaces) - } - .with_current_context(); - - let output_messages = vec![format!("Listing Spaces...\n",)]; - - let progress_output = opts.terminal.loop_messages(&output_messages, &is_finished); - - let (spaces, _) = try_join!(get_spaces, progress_output)?; + let spaces = { + let pb = opts.terminal.progress_bar(); + if let Some(pb) = pb.as_ref() { + pb.set_message("Listing spaces..."); + } + node.get_spaces(ctx).await? + }; let plain = opts.terminal.build_list( &spaces, "No spaces found. Run 'ockam enroll' to get a space and a project", )?; - let json = serde_json::to_string(&spaces).into_diagnostic()?; opts.terminal .stdout() .plain(plain) - .json(json) + .json_obj(&spaces)? .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/space/mod.rs b/implementations/rust/ockam/ockam_command/src/space/mod.rs index e2aac33089c..fd68e62951c 100644 --- a/implementations/rust/ockam/ockam_command/src/space/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/space/mod.rs @@ -5,7 +5,7 @@ pub use delete::DeleteCommand; pub use list::ListCommand; pub use show::ShowCommand; -use crate::{docs, CommandGlobalOpts}; +use crate::{docs, Command, CommandGlobalOpts}; mod create; mod delete; diff --git a/implementations/rust/ockam/ockam_command/src/space/show.rs b/implementations/rust/ockam/ockam_command/src/space/show.rs index ebca02d56b8..b0fd8a1a7c1 100644 --- a/implementations/rust/ockam/ockam_command/src/space/show.rs +++ b/implementations/rust/ockam/ockam_command/src/space/show.rs @@ -1,18 +1,16 @@ +use async_trait::async_trait; use clap::Args; use console::Term; -use miette::IntoDiagnostic; -use crate::{docs, CommandGlobalOpts}; +use crate::{docs, Command, CommandGlobalOpts}; use ockam::Context; use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; use ockam_api::terminal::{Terminal, TerminalStream}; -use ockam_core::AsyncTryClone; use crate::shared_args::IdentityOpts; use crate::terminal::tui::ShowCommandTui; use crate::tui::PluralTerm; -use crate::util::async_cmd; use ockam_api::output::Output; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); @@ -35,41 +33,29 @@ pub struct ShowCommand { pub identity_opts: IdentityOpts, } -impl ShowCommand { - pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { - async_cmd(&self.name(), opts.clone(), |ctx| async move { - self.async_run(&ctx, opts).await - }) - } - - pub fn name(&self) -> String { - "space show".into() - } +#[async_trait] +impl Command for ShowCommand { + const NAME: &'static str = "space show"; - async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { - ShowTui::run( - ctx.async_try_clone().await.into_diagnostic()?, - opts, - self.clone(), - ) - .await + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + Ok(ShowTui::run(ctx, opts, self).await?) } } -pub struct ShowTui { - ctx: Context, +pub struct ShowTui<'a> { + ctx: &'a Context, opts: CommandGlobalOpts, space_name: Option, node: InMemoryNode, } -impl ShowTui { +impl<'a> ShowTui<'a> { pub async fn run( - ctx: Context, + ctx: &'a Context, opts: CommandGlobalOpts, cmd: ShowCommand, ) -> miette::Result<()> { - let node = InMemoryNode::start(&ctx, &opts.state).await?; + let node = InMemoryNode::start(ctx, &opts.state).await?; let tui = Self { ctx, opts, @@ -81,7 +67,7 @@ impl ShowTui { } #[ockam_core::async_trait] -impl ShowCommandTui for ShowTui { +impl<'a> ShowCommandTui for ShowTui<'a> { const ITEM_NAME: PluralTerm = PluralTerm::Space; fn cmd_arg_item_name(&self) -> Option { @@ -103,7 +89,7 @@ impl ShowCommandTui for ShowTui { async fn list_items_names(&self) -> miette::Result> { Ok(self .node - .get_spaces(&self.ctx) + .get_spaces(self.ctx) .await? .iter() .map(|s| s.space_name()) @@ -111,11 +97,11 @@ impl ShowCommandTui for ShowTui { } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let space = self.node.get_space_by_name(&self.ctx, item_name).await?; + let space = self.node.get_space_by_name(self.ctx, item_name).await?; self.terminal() .stdout() .plain(space.item()?) - .json(serde_json::to_string(&space).into_diagnostic()?) + .json_obj(&space)? .machine(&space.name) .write_line()?; Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/status.rs b/implementations/rust/ockam/ockam_command/src/status.rs index 46039e8bd6c..42195217e3e 100644 --- a/implementations/rust/ockam/ockam_command/src/status.rs +++ b/implementations/rust/ockam/ockam_command/src/status.rs @@ -7,19 +7,20 @@ use miette::IntoDiagnostic; use serde::Serialize; use tracing::warn; +use crate::node::show::get_node_resources; +use crate::shared_args::TimeoutArg; +use crate::Result; +use crate::{Command, CommandGlobalOpts}; use ockam::Context; use ockam_api::cli_state::{EnrollmentFilter, IdentityEnrollment}; use ockam_api::cloud::project::models::OrchestratorVersionInfo; +use ockam_api::cloud::space::Space; use ockam_api::colors::color_primary; use ockam_api::nodes::models::node::NodeResources; use ockam_api::nodes::{BackgroundNodeClient, InMemoryNode}; +use ockam_api::output::Output; use ockam_api::{fmt_heading, fmt_log, fmt_separator, fmt_warn}; -use crate::node::show::get_node_resources; -use crate::shared_args::TimeoutArg; -use crate::Result; -use crate::{Command, CommandGlobalOpts}; - /// Display information about the system's status #[derive(Clone, Debug, Args)] pub struct StatusCommand { @@ -34,18 +35,18 @@ impl Command for StatusCommand { async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> Result<()> { let identities_details = self.get_identities_details(&opts).await?; let nodes = self.get_nodes_resources(ctx, &opts).await?; - let orchestrator_version = { - let node = InMemoryNode::start(ctx, &opts.state) - .await? - .with_timeout(self.timeout.timeout); - let controller = node.create_controller().await?; - controller - .get_orchestrator_version_info(ctx) - .await - .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) - .unwrap_or_default() - }; - let status = StatusData::from_parts(orchestrator_version, identities_details, nodes)?; + let node = InMemoryNode::start(ctx, &opts.state) + .await? + .with_timeout(self.timeout.timeout); + let controller = node.create_controller().await?; + let orchestrator_version = controller + .get_orchestrator_version_info(ctx) + .await + .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) + .unwrap_or_default(); + let spaces = opts.state.get_spaces().await?; + let status = + StatusData::from_parts(orchestrator_version, spaces, identities_details, nodes)?; opts.terminal .stdout() .plain(&status) @@ -93,6 +94,7 @@ impl StatusCommand { struct StatusData { #[serde(flatten)] orchestrator_version: OrchestratorVersionInfo, + spaces: Vec, identities: Vec, nodes: Vec, } @@ -100,11 +102,13 @@ struct StatusData { impl StatusData { fn from_parts( orchestrator_version: OrchestratorVersionInfo, + spaces: Vec, identities: Vec, nodes: Vec, ) -> Result { Ok(Self { orchestrator_version, + spaces, identities, nodes, }) @@ -130,6 +134,24 @@ impl Display for StatusData { ) )?; + if self.spaces.is_empty() { + writeln!(f, "{}", fmt_separator!())?; + writeln!(f, "{}", fmt_warn!("No spaces found"))?; + writeln!( + f, + "{}", + fmt_log!("Consider running `ockam enroll` or `ockam space create` create your first space.") + )?; + } else { + writeln!(f, "{}", fmt_heading!("Spaces"))?; + for (idx, space) in self.spaces.iter().enumerate() { + if idx > 0 { + writeln!(f)?; + } + writeln!(f, "{}", space.iter_output()?.pad())?; + } + } + if self.identities.is_empty() { writeln!(f, "{}", fmt_separator!())?; writeln!(f, "{}", fmt_warn!("No identities found"))?; diff --git a/implementations/rust/ockam/ockam_command/src/subscription.rs b/implementations/rust/ockam/ockam_command/src/subscription.rs index 6959c123ea9..7651b6568ed 100644 --- a/implementations/rust/ockam/ockam_command/src/subscription.rs +++ b/implementations/rust/ockam/ockam_command/src/subscription.rs @@ -3,7 +3,7 @@ use clap::{Args, Subcommand}; use miette::{miette, IntoDiagnostic}; use ockam::Context; -use ockam_api::cloud::subscription::{Subscription, Subscriptions}; +use ockam_api::cloud::subscription::{SubscriptionLegacy, Subscriptions}; use ockam_api::cloud::ControllerClient; use ockam_api::nodes::InMemoryNode; @@ -92,7 +92,7 @@ pub(crate) async fn get_subscription_by_id_or_space_id( ctx: &Context, subscription_id: Option, space_id: Option, -) -> Result> { +) -> Result> { match (subscription_id, space_id) { (Some(subscription_id), _) => Ok(Some( controller diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240703100000_add_subscription_to_space.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240703100000_add_subscription_to_space.sql new file mode 100644 index 00000000000..e798ac57138 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240703100000_add_subscription_to_space.sql @@ -0,0 +1,3 @@ +-- This migration adds a subscription field to the space table +ALTER TABLE space + ADD subscription TEXT; diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240703100000_add_subscription_to_space.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240703100000_add_subscription_to_space.sql new file mode 100644 index 00000000000..12bca319e0f --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240703100000_add_subscription_to_space.sql @@ -0,0 +1,3 @@ +-- This migration adds a subscription field to the space table +ALTER TABLE space + ADD subscription TEXT; -- optional field for subscription information attached to a space