From d7c9f1e1133ad9e5a43fa0db6cd248f9fec7698d Mon Sep 17 00:00:00 2001 From: Adrian Benavides Date: Fri, 22 Nov 2024 10:24:13 +0100 Subject: [PATCH] feat: adjust `enroll` logic and output for the new subscription plans --- .../storage/spaces_repository_sql.rs | 14 +- .../rust/ockam/ockam_api/src/cloud/space.rs | 119 +++++++--- .../ockam/ockam_api/src/cloud/subscription.rs | 109 +++++---- .../rust/ockam/ockam_api/src/date.rs | 174 +++++++++++++++ .../ockam/ockam_api/src/schema/schema.cddl | 4 +- .../ockam/ockam_api/src/ui/terminal/mod.rs | 4 +- .../ockam_api/src/ui/terminal/notification.rs | 2 +- .../ockam/ockam_command/src/enroll/command.rs | 209 +++++++++++++----- .../ockam_command/src/enroll/oidc_service.rs | 10 +- .../src/influxdb/inlet/create.rs | 2 +- .../src/influxdb/outlet/create.rs | 2 +- .../ockam_command/src/kafka/inlet/create.rs | 2 +- .../ockam_command/src/kafka/outlet/create.rs | 2 +- .../ockam/ockam_command/src/operation/util.rs | 4 +- .../ockam/ockam_command/src/project/enroll.rs | 6 +- .../ockam/ockam_command/src/project/ticket.rs | 2 +- .../ockam/ockam_command/src/project/util.rs | 2 +- .../src/project_member/delete.rs | 2 +- .../ockam/ockam_command/src/relay/create.rs | 2 +- .../ockam/ockam_command/src/relay/list.rs | 2 +- .../rust/ockam/ockam_command/src/reset.rs | 2 +- .../ockam/ockam_command/src/space/create.rs | 4 +- .../ockam/ockam_command/src/space/list.rs | 2 +- .../rust/ockam/ockam_command/src/status.rs | 2 +- .../ockam_command/src/tcp/inlet/create.rs | 2 +- .../ockam/ockam_command/src/tcp/inlet/list.rs | 2 +- .../ockam_command/src/tcp/outlet/create.rs | 2 +- 27 files changed, 511 insertions(+), 178 deletions(-) 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 d7e159b7601..88d87bce212 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 @@ -116,8 +116,8 @@ impl SpacesRepository for SpacesSqlxDatabase { .bind(&subscription.name) .bind(subscription.is_free_trial) .bind(&subscription.marketplace) - .bind(start_date.map(|d| d.unix_timestamp())) - .bind(end_date.map(|d| d.unix_timestamp())); + .bind(start_date.map(|d| d.into_inner().unix_timestamp())) + .bind(end_date.map(|d| d.into_inner().unix_timestamp())); query.execute(&mut *transaction).await.void()?; } // remove the subscription @@ -307,10 +307,12 @@ impl SubscriptionRow { self.marketplace.to_option(), self.start_date .to_option() - .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()), + .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()) + .and_then(|t| t.try_into().ok()), self.end_date .to_option() - .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()), + .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()) + .and_then(|t| t.try_into().ok()), ) } } @@ -346,8 +348,8 @@ mod test { "premium".to_string(), false, Some("aws".to_string()), - Some(OffsetDateTime::now_utc()), - Some(OffsetDateTime::now_utc().add(2.days())), + Some(OffsetDateTime::now_utc().try_into().unwrap()), + Some(OffsetDateTime::now_utc().add(2.days()).try_into().unwrap()), )), }; diff --git a/implementations/rust/ockam/ockam_api/src/cloud/space.rs b/implementations/rust/ockam/ockam_api/src/cloud/space.rs index ab82fde391c..2401d2d679a 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/space.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/space.rs @@ -10,7 +10,7 @@ use ockam_node::Context; use crate::cloud::email_address::EmailAddress; use crate::cloud::project::models::AdminInfo; use crate::cloud::project::{Project, ProjectsOrchestratorApi}; -use crate::cloud::subscription::Subscription; +use crate::cloud::subscription::{Subscription, SUBSCRIPTION_PAGE}; use crate::cloud::{ControllerClient, HasSecureClient}; use crate::colors::{color_primary, color_uri, color_warn}; use crate::fmt_log; @@ -39,8 +39,11 @@ impl Space { self.name.clone() } - pub fn has_subscription(&self) -> bool { - self.subscription.is_some() + pub fn has_valid_subscription(&self) -> bool { + self.subscription + .as_ref() + .map(|s| s.is_valid()) + .unwrap_or(false) } pub fn is_in_free_trial_subscription(&self) -> bool { @@ -52,46 +55,40 @@ impl Space { .unwrap_or_default() } - pub fn subscription_status_message(&self, space_is_new: bool) -> crate::Result { + pub fn subscription_status_message(&self) -> crate::Result { let mut f = String::new(); if let Some(subscription) = &self.subscription { + let trial_text = if subscription.is_free_trial { + "Trial of the " + } else { + "" + }; writeln!( f, "{}", fmt_log!( - "This Space has a {} Subscription attached to it.", - color_primary(&subscription.name) + "This Space has a {}{} Subscription attached to it.", + trial_text, + subscription.name_colored(), ) )?; + if let (Some(start_date), Some(end_date)) = + (&subscription.start_date(), &subscription.end_date()) + { + writeln!( + f, + "{}", + fmt_log!( + "Your trial started on {} and will end in {}.", + color_primary(start_date.format_human()?), + color_primary(end_date.diff_human(start_date)) + ) + )?; + } if subscription.is_free_trial { - if space_is_new { - writeln!(f)?; - writeln!(f, "{}", fmt_log!("As a courtesy, we created a temporary Space for you, so you can continue to build.\n"))?; - writeln!( - f, - "{}", - fmt_log!( - "Please subscribe to an Ockam plan within two weeks {}", - color_uri("https://www.ockam.io/pricing") - ) - )?; - writeln!(f, "{}", fmt_log!("{}", color_warn("If you don't subscribe in that time, your Space and all associated Projects will be permanently deleted.")))?; - } else if let (Some(start_date), Some(end_date)) = - (&subscription.start_date(), &subscription.end_date()) - { - writeln!(f)?; - writeln!( - f, - "{}", - fmt_log!( - "Your free trial started on {} and will end on {}.\n", - start_date, - end_date - ) - )?; - writeln!(f, "{}", fmt_log!("Please subscribe to an Ockam plan before the trial ends to avoid any service interruptions {}", color_uri("https://www.ockam.io/pricing")))?; - writeln!(f, "{}", fmt_log!("{}", color_warn("If you don't subscribe in that time, your Space and all associated Projects will be permanently deleted.")))?; - } + writeln!(f)?; + writeln!(f, "{}", fmt_log!("Please go to {} and subscribe before the trial ends to avoid any service interruptions.", color_uri(SUBSCRIPTION_PAGE)))?; + writeln!(f, "{}", fmt_log!("{}", color_warn("If you don't subscribe in that time, your Space and all associated Projects will be permanently deleted.")))?; } } else { writeln!( @@ -362,10 +359,10 @@ impl ControllerClient { #[cfg(test)] pub mod tests { - use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; - use crate::cloud::space::CreateSpace; use crate::schema::tests::validate_with_schema; + use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; + use time::OffsetDateTime; use super::*; @@ -402,4 +399,54 @@ pub mod tests { } } } + + #[test] + fn valid_subscription_check() { + let mut g = Gen::new(100); + let mut space = Space::arbitrary(&mut g); + + // No subscription + space.subscription = None; + assert!(!space.has_valid_subscription()); + + // Paid subscription + let mut sub = Subscription::arbitrary(&mut g); + sub.is_free_trial = false; + space.subscription = Some(sub.clone()); + assert!(space.has_valid_subscription()); + + // Trial subscription with no dates + let mut sub = Subscription::arbitrary(&mut g); + sub.is_free_trial = true; + sub.start_date = None; + sub.end_date = None; + space.subscription = Some(sub.clone()); + assert!(!space.has_valid_subscription()); + + // Trial subscription with date values + sub.start_date = Some( + (OffsetDateTime::now_utc() - time::Duration::days(1)) + .try_into() + .unwrap(), + ); + sub.end_date = None; + space.subscription = Some(sub.clone()); + assert!(!space.has_valid_subscription()); + + sub.end_date = Some( + (OffsetDateTime::now_utc() - time::Duration::hours(1)) + .try_into() + .unwrap(), + ); + space.subscription = Some(sub.clone()); + assert!(!space.has_valid_subscription()); + + sub.end_date = Some( + (OffsetDateTime::now_utc() + time::Duration::hours(1)) + .try_into() + .unwrap(), + ); + space.subscription = Some(sub.clone()); + assert!(space.has_valid_subscription()); + } } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs b/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs index e2eff71ca4f..0aab30ac3ae 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/subscription.rs @@ -1,13 +1,12 @@ use crate::cloud::{ControllerClient, HasSecureClient}; use crate::output::Output; +use colorful::{Colorful, RGB}; use minicbor::{CborLen, Decode, Encode}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter, Write}; -use time::format_description::well_known::Iso8601; -use time::OffsetDateTime; -use crate::colors::color_primary; -use crate::date::parse_date; +use crate::colors::{color_primary, OckamColor}; +use crate::date::UtcDateTime; use crate::terminal::fmt; use ockam_core::api::{Error, Reply, Request, Status}; use ockam_core::{self, async_trait, Result}; @@ -16,6 +15,8 @@ use ockam_node::Context; const TARGET: &str = "ockam_api::cloud::subscription"; const API_SERVICE: &str = "subscriptions"; +pub const SUBSCRIPTION_PAGE: &str = "https://orchestrator.ockam.io"; + #[derive(Encode, Decode, CborLen, Debug)] #[cfg_attr(test, derive(Clone))] #[rustfmt::skip] @@ -54,38 +55,19 @@ impl ActivateSubscription { } } -#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Clone, Debug, Eq)] +#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[cbor(map)] pub struct Subscription { #[n(1)] - pub name: String, + pub name: String, // TODO: Use enum for known values? #[n(2)] pub is_free_trial: bool, #[n(3)] pub marketplace: Option, #[n(4)] - start_date: Option, + pub(crate) start_date: Option, #[n(5)] - end_date: Option, -} - -impl PartialEq for Subscription { - fn eq(&self, other: &Self) -> bool { - // Compare the dates using as unix timestamps, using a tolerance of 1 second - let start_date_eq = match (self.start_date(), other.start_date()) { - (Some(start_date), Some(other_start_date)) => { - let start_date = start_date.unix_timestamp(); - let other_start_date = other_start_date.unix_timestamp(); - (start_date - other_start_date).abs() <= 1 - } - (None, None) => true, - _ => false, - }; - self.name == other.name - && self.is_free_trial == other.is_free_trial - && self.marketplace == other.marketplace - && start_date_eq - } + pub(crate) end_date: Option, } impl Subscription { @@ -93,47 +75,80 @@ impl Subscription { name: String, is_free_trial: bool, marketplace: Option, - start_date: Option, - end_date: Option, + start_date: Option, + end_date: Option, ) -> Self { Self { name, is_free_trial, marketplace, - start_date: start_date.and_then(|date| date.format(&Iso8601::DEFAULT).ok()), - end_date: end_date.and_then(|date| date.format(&Iso8601::DEFAULT).ok()), + start_date, + end_date, + } + } + + pub fn end_date(&self) -> Option { + self.end_date.clone() + } + + pub fn start_date(&self) -> Option { + self.start_date.clone() + } + + /// A subscription is valid if: + /// - is in a trial period and end date is in the future + /// - a plan is not in trial status + pub fn is_valid(&self) -> bool { + if self.is_free_trial { + self.end_date() + .map(|end_date| end_date.is_in_the_future()) + .unwrap_or(false) + } else { + true } } - pub fn end_date(&self) -> Option { - self.end_date - .as_ref() - .and_then(|date| parse_date(date).ok()) + pub fn grace_period_end_date(&self) -> crate::Result> { + if !self.is_free_trial { + return Ok(None); + } + match self.end_date.as_ref() { + Some(end_date) => { + let grace_period = time::Duration::days(3); + let end_date = end_date.clone().into_inner() + grace_period; + Ok(Some(UtcDateTime::new(end_date)?)) + } + None => Ok(None), + } } - pub fn start_date(&self) -> Option { - self.start_date - .as_ref() - .and_then(|date| parse_date(date).ok()) + pub fn name_colored(&self) -> String { + let color = match self.name.to_lowercase().as_str() { + "gold" => RGB::new(255, 215, 0), + "silver" => RGB::new(230, 232, 250), + "bronze" => RGB::new(140, 120, 83), + _ => OckamColor::PrimaryResource.color(), + }; + self.name.clone().color(color).to_string() } } 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)")?; + let trial_text = if self.is_free_trial { + "Trial of the " } else { - writeln!(f)?; - } + "" + }; + writeln!(f, "{}{} Subscription", trial_text, self.name_colored())?; if let (Some(start_date), Some(end_date)) = (self.start_date(), self.end_date()) { writeln!( f, - "{}Started at {}, expires at {}", + "{}Started on {}, expires in {}", fmt::INDENTATION, - color_primary(start_date.to_string()), - color_primary(end_date.to_string()), + color_primary(start_date.format_human()?), + color_primary(end_date.diff_human(&start_date)), )?; } diff --git a/implementations/rust/ockam/ockam_api/src/date.rs b/implementations/rust/ockam/ockam_api/src/date.rs index 81d36dfc984..c38b2719a15 100644 --- a/implementations/rust/ockam/ockam_api/src/date.rs +++ b/implementations/rust/ockam/ockam_api/src/date.rs @@ -1,5 +1,10 @@ use crate::ApiError; +use minicbor::{decode, encode, CborLen, Decode, Decoder, Encode, Encoder}; +use serde::ser::Error; +use serde::{Deserialize, Serialize, Serializer}; +use std::str::FromStr; use time::format_description::well_known::Iso8601; +use time::macros::{format_description, offset}; use time::OffsetDateTime; /// Transform a string that represents an Iso8601 date into a `time::OffsetDateTime` @@ -19,3 +24,172 @@ pub fn is_expired(date: &str) -> ockam_core::Result { let now = OffsetDateTime::now_utc(); Ok(date < now) } + +#[derive(Clone, Debug, Eq)] +pub struct UtcDateTime(OffsetDateTime); + +impl UtcDateTime { + pub fn new(date: OffsetDateTime) -> crate::Result { + if date.offset() != offset!(UTC) { + return Err(ApiError::General("The date must be in UTC".to_string())); + } + Ok(Self(date)) + } + + pub fn is_in_the_past(&self) -> bool { + self.0 < OffsetDateTime::now_utc() + } + + pub fn is_in_the_future(&self) -> bool { + !self.is_in_the_past() + } + + /// If the date is "2024-10-01T00:00:00Z", this will return "10, Dec 2024" + pub fn format_human(&self) -> Result { + self.0 + .format(format_description!("[day], [month repr:short] [year]")) + .map_err(|e| std::fmt::Error::custom(e.to_string())) + } + + /// Return the number of days in human format. For example, "3 days" + pub fn diff_human(&self, other: &Self) -> String { + let diff = self.0 - other.0; + format!("{} days", diff.whole_days()) + } + + pub fn into_inner(self) -> OffsetDateTime { + self.0 + } +} + +impl TryFrom for UtcDateTime { + type Error = ApiError; + + fn try_from(date: OffsetDateTime) -> Result { + Self::new(date) + } +} + +impl FromStr for UtcDateTime { + type Err = ApiError; + + fn from_str(date: &str) -> 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) + }; + Self::new( + OffsetDateTime::parse(&date, &Iso8601::DEFAULT) + .map_err(|e| ApiError::core(e.to_string()))?, + ) + } +} + +impl PartialEq for UtcDateTime { + fn eq(&self, other: &Self) -> bool { + // Compare the dates using as unix timestamps, using a tolerance of 1 second + let start_date = self.0.unix_timestamp(); + let other_start_date = other.0.unix_timestamp(); + (start_date - other_start_date).abs() <= 1 + } +} + +impl Encode for UtcDateTime { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut C, + ) -> Result<(), encode::Error> { + self.0.unix_timestamp().encode(e, ctx) + } +} + +impl CborLen for UtcDateTime { + fn cbor_len(&self, ctx: &mut C) -> usize { + self.0.unix_timestamp().cbor_len(ctx) + } +} + +impl<'b, C> Decode<'b, C> for UtcDateTime { + fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result { + let timestamp = d.decode_with(ctx)?; + let inner = + OffsetDateTime::from_unix_timestamp(timestamp).map_err(decode::Error::message)?; + Ok(Self(inner)) + } +} + +impl Serialize for UtcDateTime { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.unix_timestamp().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for UtcDateTime { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let timestamp = i64::deserialize(deserializer)?; + let inner = + OffsetDateTime::from_unix_timestamp(timestamp).map_err(serde::de::Error::custom)?; + Ok(UtcDateTime(inner)) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; + + impl Arbitrary for UtcDateTime { + fn arbitrary(g: &mut Gen) -> Self { + // Use i32 instead of i64 to avoid using values larger than time::Date::MAX + UtcDateTime(OffsetDateTime::from_unix_timestamp(i32::arbitrary(g) as i64).unwrap()) + } + } + + quickcheck! { + fn utc_date_time(unix_timestamp: i32) -> TestResult { + let inner = OffsetDateTime::from_unix_timestamp(unix_timestamp as i64).unwrap(); + match UtcDateTime::new(inner) { + Ok(_) => TestResult::passed(), + Err(e) => TestResult::error(format!("{e:?}")), + } + } + } + + #[test] + fn test_utc_date_time() { + let date = UtcDateTime::from_str("2024-10-01T00:00:00Z").unwrap(); + assert_eq!(date.format_human().unwrap(), "01, Oct 2024"); + let without_timezone = UtcDateTime::from_str("2024-10-01T00:00:00").unwrap(); + assert_eq!(without_timezone, date); + let from_inner = OffsetDateTime::from_unix_timestamp(date.0.unix_timestamp()).unwrap(); + assert_eq!(UtcDateTime::try_from(from_inner).unwrap(), date); + // Fail if the date is not in UTC + assert!(UtcDateTime::from_str("2024-10-01T00:00:00+01:00").is_err()); + } + + #[test] + fn utc_date_time_cbor_encode_decode() { + let date = UtcDateTime::from_str("2024-10-01T00:00:00Z").unwrap(); + let mut bytes = Vec::new(); + date.encode(&mut Encoder::new(&mut bytes), &mut ()).unwrap(); + let decoded = UtcDateTime::decode(&mut Decoder::new(&bytes), &mut ()).unwrap(); + assert_eq!(decoded, date); + } + + #[test] + fn utc_date_time_serde() { + let date = UtcDateTime::from_str("2024-10-01T00:00:00Z").unwrap(); + let serialized = serde_json::to_string(&date).unwrap(); + let deserialized: UtcDateTime = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, date); + } +} diff --git a/implementations/rust/ockam/ockam_api/src/schema/schema.cddl b/implementations/rust/ockam/ockam_api/src/schema/schema.cddl index c4339e35f90..d505a4662df 100644 --- a/implementations/rust/ockam/ockam_api/src/schema/schema.cddl +++ b/implementations/rust/ockam/ockam_api/src/schema/schema.cddl @@ -238,8 +238,8 @@ subscription = { 1: text, ;; name 2: bool, ;; is_free_trial ?3: text, ;; marketplace - ?4: text, ;; start_date - ?5: text, ;; end_date + ?4: int, ;; start_date + ?5: int, ;; end_date } subscription_legacy = { diff --git a/implementations/rust/ockam/ockam_api/src/ui/terminal/mod.rs b/implementations/rust/ockam/ockam_api/src/ui/terminal/mod.rs index ffc401371f3..89d83c03763 100644 --- a/implementations/rust/ockam/ockam_api/src/ui/terminal/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/ui/terminal/mod.rs @@ -394,7 +394,7 @@ impl Terminal { self.stderr.is_tty() && self.can_write_to_stderr() } - pub fn progress_bar(&self) -> Option { + pub fn spinner(&self) -> Option { if !self.can_use_progress_bar() { return None; } @@ -424,7 +424,7 @@ impl Terminal { if output_messages.is_empty() { return Ok(()); } - let pb = match self.progress_bar() { + let pb = match self.spinner() { Some(pb) => pb, None => return Ok(()), }; diff --git a/implementations/rust/ockam/ockam_api/src/ui/terminal/notification.rs b/implementations/rust/ockam/ockam_api/src/ui/terminal/notification.rs index 039193bd389..0524986b588 100644 --- a/implementations/rust/ockam/ockam_api/src/ui/terminal/notification.rs +++ b/implementations/rust/ockam/ockam_api/src/ui/terminal/notification.rs @@ -120,7 +120,7 @@ impl NotificationHandler { Notification::Progress(contents) => { if self.terminal.can_use_progress_bar() { if self.progress_bar.is_none() { - self.progress_bar = self.terminal.progress_bar(); + self.progress_bar = self.terminal.spinner(); } if let Some(pb) = self.progress_bar.as_ref() { pb.set_message(contents); diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index ab38f83f9a7..0bf41d2a009 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::io::stdin; use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -22,18 +23,18 @@ 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; use ockam_api::cloud::enroll::auth0::*; use ockam_api::cloud::project::Project; use ockam_api::cloud::project::ProjectsOrchestratorApi; use ockam_api::cloud::space::{Space, Spaces}; +use ockam_api::cloud::subscription::SUBSCRIPTION_PAGE; use ockam_api::cloud::ControllerClient; use ockam_api::colors::{color_primary, color_uri, OckamColor}; use ockam_api::enroll::enrollment::{EnrollStatus, Enrollment}; use ockam_api::enroll::oidc_service::OidcService; use ockam_api::nodes::InMemoryNode; use ockam_api::terminal::notification::NotificationHandler; -use ockam_api::{fmt_log, fmt_ok, fmt_warn}; +use ockam_api::{fmt_err, fmt_log, fmt_ok, fmt_warn}; use ockam_api::{fmt_separator, CliState}; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); @@ -205,7 +206,7 @@ impl EnrollCommand { cli_state: &CliState, opts: &CommandGlobalOpts, ) -> miette::Result { - let is_already_enrolled = !cli_state + let mut is_already_enrolled = !cli_state .identity_should_enroll(&self.identity, false) .await?; if is_already_enrolled { @@ -244,6 +245,17 @@ impl EnrollCommand { } }; } + + // Check if the default space is available and has a valid subscription + let default_space = match cli_state.get_default_space().await { + Ok(space) => space, + Err(_) => { + // If there is no default space, we want to continue with the enrollment process + return Ok(false); + } + }; + is_already_enrolled &= default_space.has_valid_subscription(); + Ok(is_already_enrolled) } @@ -403,74 +415,161 @@ async fn get_user_space( skip_orchestrator_resources_creation: bool, ) -> miette::Result> { // Get the available spaces for node's identity - // Those spaces might have been created previously and all the local state reset opts.terminal.write_line(fmt_log!( "Getting available Spaces accessible to your account." ))?; - let is_finished = Mutex::new(false); - let get_spaces = async { - let spaces = node.get_spaces(ctx).await?; - *is_finished.lock().await = true; - Ok(spaces) - }; - let message = vec!["Checking for any existing Spaces...".to_string()]; - let progress_output = opts.terminal.loop_messages(&message, &is_finished); + let spaces = { + let sp = opts.terminal.spinner(); + if let Some(spinner) = sp.as_ref() { + spinner.set_message("Checking for any existing Spaces..."); + } + node.get_spaces(ctx).await? + }; - 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() { + // If the identity has no spaces, create one None => { - if skip_orchestrator_resources_creation { - opts.terminal - .write_line(fmt_log!("No Spaces are accessible to your account.\n"))?; - return Ok(None); - } - + // send user to subscription page + opts.terminal + .write_line(fmt_log!("No Spaces are accessible to your account.\n"))?; opts.terminal.write_line(fmt_log!( - "No Spaces are accessible to your account, creating a new one..." + "Please go to {} and subscribe to create a new Space.", + color_uri(SUBSCRIPTION_PAGE) ))?; - let is_finished = Mutex::new(false); - let space_name = random_name(); - let create_space = async { - let space = node.create_space(ctx, &space_name, vec![]).await?; - *is_finished.lock().await = true; - Ok(space) - }; + if skip_orchestrator_resources_creation { + return Ok(None); + } - let message = vec![format!( - "Creating a new Space {}...", - color_primary(space_name.clone()) - )]; - let progress_output = opts.terminal.loop_messages(&message, &is_finished); - let (space, _) = try_join!(create_space, progress_output)?; - opts.terminal.write_line(fmt_ok!( - "Created a new Space named {}.", - color_primary(space.name.clone()) - ))?; - is_new = true; - space + ask_user_to_subscribe_and_wait_for_space_to_be_ready(opts, ctx, node).await? } Some(space) => { opts.terminal.write_line(fmt_log!( - "Found existing Space {}.", - color_primary(space.name.clone()) + "Found existing Space {}.\n", + color_primary(&space.name) ))?; - space.clone() + match &space.subscription { + // if no subscription is attached to the space, ask the user to subscribe + None => { + opts.terminal.write_line(fmt_log!( + "Your Space {} doesn't have a Subscription attached to it.", + color_primary(&space.name) + ))?; + opts.terminal.write_line(fmt_log!( + "Please go to {} and subscribe to use your Space.", + color_uri(SUBSCRIPTION_PAGE) + ))?; + ask_user_to_subscribe_and_wait_for_space_to_be_ready(opts, ctx, node).await? + } + Some(subscription) => { + // if there is a subscription, check that it's not expired + if !subscription.is_valid() { + opts.terminal.write_line(fmt_log!( + "Your Trial of the {} Subscription on the Space {} has ended.", + subscription.name_colored(), + color_primary(&space.name) + ))?; + opts.terminal.write_line(fmt_log!( + "Please go to {} and subscribe to one of our paid plans to use your Space.", + color_uri(SUBSCRIPTION_PAGE) + ))?; + if let Some(grace_period_end_date) = subscription.grace_period_end_date()? { + let date = color_primary( + grace_period_end_date.format_human().into_diagnostic()?, + ); + if grace_period_end_date.is_in_the_past() { + opts.terminal.write_line(fmt_log!( + "All Projects in this Space were deleted on {date}." + ))?; + } else { + opts.terminal.write_line(fmt_log!( + "All Projects in this Space will be deleted on {date}." + ))?; + } + } + ask_user_to_subscribe_and_wait_for_space_to_be_ready(opts, ctx, node) + .await? + } + // otherwise return the space as is + else { + space.clone() + } + } + } } }; + space.subscription.as_ref().ok_or_else(|| { + // At this point, the space should have a subscription, but just in case + miette!( + "Please go to {} and try again", + color_uri(SUBSCRIPTION_PAGE) + ) + .wrap_err("The Space does not have a subscription plan attached.") + })?; opts.terminal.write_line(fmt_ok!( "Marked {} as your default Space, on this machine.\n", - color_primary(space.name.clone()) + color_primary(&space.name) ))?; - if let Ok(msg) = space.subscription_status_message(is_new) { + if let Ok(msg) = space.subscription_status_message() { opts.terminal.write_line(msg)?; } Ok(Some(space)) } +async fn ask_user_to_subscribe_and_wait_for_space_to_be_ready( + opts: &CommandGlobalOpts, + ctx: &Context, + node: &InMemoryNode, +) -> Result { + opts.terminal.write_line("")?; + if opts.terminal.can_ask_for_user_input() { + opts.terminal.write(fmt_log!( + "Press {} to open {} in your browser.", + " ENTER ↵ ".bg_white().black().blink(), + color_uri(SUBSCRIPTION_PAGE) + ))?; + + let mut input = String::new(); + match stdin().read_line(&mut input) { + Ok(_) => { + opts.terminal + .write_line(fmt_log!("Opening your browser..."))?; + } + Err(_e) => { + return Err(miette!( + "Couldn't read user input or enter keypress from stdin" + ))?; + } + } + } + if open::that(SUBSCRIPTION_PAGE).is_err() { + opts.terminal.write_line(fmt_err!( + "Couldn't open your browser from the terminal. Please open {} manually.", + color_uri(SUBSCRIPTION_PAGE) + ))?; + } + + opts.terminal.write_line("")?; + + // wait until the user has subscribed and a space is created + let sp = opts.terminal.spinner(); + if let Some(spinner) = sp.as_ref() { + let msg = "Waiting for you to subscribe using your browser..."; + spinner.set_message(msg); + } + let space = loop { + let spaces = node.get_spaces(ctx).await?; + if let Some(space) = spaces.into_iter().next() { + if space.has_valid_subscription() { + break space; + } + } + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + }; + Ok(space) +} + async fn get_user_project( opts: &CommandGlobalOpts, ctx: &Context, @@ -478,24 +577,20 @@ async fn get_user_project( skip_orchestrator_resources_creation: bool, space: &Space, ) -> Result> { - // Get available project for the given space + // Get available projects for the given space opts.terminal.write_line(fmt_log!( "Getting available Projects in the Space {}...", color_primary(&space.name) ))?; - let is_finished = Mutex::new(false); - let get_projects = async { - let projects = node.get_admin_projects(ctx).await?; - *is_finished.lock().await = true; - Ok(projects) + let projects = { + let sp = opts.terminal.spinner(); + if let Some(spinner) = sp.as_ref() { + spinner.set_message("Checking for any existing Projects..."); + } + node.get_admin_projects(ctx).await? }; - let message = vec!["Checking for existing Projects...".to_string()]; - let progress_output = opts.terminal.loop_messages(&message, &is_finished); - - let (projects, _) = try_join!(get_projects, progress_output)?; - // If the space has no projects, create one let project = match projects.first() { None => { diff --git a/implementations/rust/ockam/ockam_command/src/enroll/oidc_service.rs b/implementations/rust/ockam/ockam_command/src/enroll/oidc_service.rs index 13649ece096..d87902257da 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/oidc_service.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/oidc_service.rs @@ -144,7 +144,7 @@ impl OidcServiceExt for OidcService { token: &OidcToken, terminal: Option<&Terminal>>, ) -> Result { - let pb = terminal.and_then(|t| t.progress_bar()); + let pb = terminal.and_then(|t| t.spinner()); if let Some(spinner) = pb.as_ref() { spinner.set_message("Verifying email..."); sleep(Duration::from_millis(500)).await; @@ -184,7 +184,7 @@ impl OidcServiceExt for OidcService { ) -> Result { if open::that(uri.clone()).is_err() { opts.terminal.write_line(fmt_err!( - "Couldn't open activation URL automatically [URL={}]", + "Couldn't open your browser from the terminal. Please open {} manually.", color_uri(&uri) ))?; } @@ -200,8 +200,8 @@ impl OidcServiceExt for OidcService { let provider = self.provider(); let client = provider.build_http_client()?; let token; - let pb = opts.terminal.progress_bar(); - if let Some(spinner) = pb.as_ref() { + let sp = opts.terminal.spinner(); + if let Some(spinner) = sp.as_ref() { let msg = format!( "{} {} {}", "Waiting for you to complete activating", @@ -229,7 +229,7 @@ impl OidcServiceExt for OidcService { StatusCode::OK => { token = res.json::().await.into_diagnostic()?; debug!(?token, "token response received"); - if let Some(spinner) = pb.as_ref() { + if let Some(spinner) = sp.as_ref() { spinner.finish_and_clear(); } return Ok(token); diff --git a/implementations/rust/ockam/ockam_command/src/influxdb/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/influxdb/inlet/create.rs index b939dc619ed..02e5770bd80 100644 --- a/implementations/rust/ockam/ockam_command/src/influxdb/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/influxdb/inlet/create.rs @@ -47,7 +47,7 @@ impl Command for InfluxDBCreateCommand { .map(|t| node.set_timeout_mut(t)); let inlet_status = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating a InfluxDB Inlet at {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/influxdb/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/influxdb/outlet/create.rs index bc1a5752abe..947242e78ba 100644 --- a/implementations/rust/ockam/ockam_command/src/influxdb/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/influxdb/outlet/create.rs @@ -72,7 +72,7 @@ impl Command for InfluxDBCreateCommand { let node = BackgroundNodeClient::create(ctx, &opts.state, &self.tcp_outlet.at).await?; let outlet_status = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating a new InfluxDB Outlet to {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs index b0fed96b913..aafd142fbaf 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs @@ -151,7 +151,7 @@ impl Command for CreateCommand { let to = process_nodes_multiaddr(&self.to, &opts.state).await?; let inlet = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating Kafka Inlet at {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs index 60bd2cf9b06..8bd0ab14ca0 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs @@ -58,7 +58,7 @@ impl Command for CreateCommand { initialize_default_node(ctx, &opts).await?; let outlet = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating Kafka Outlet to bootstrap server {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/operation/util.rs b/implementations/rust/ockam/ockam_command/src/operation/util.rs index a9adfa5e103..572b39d0d3e 100644 --- a/implementations/rust/ockam/ockam_command/src/operation/util.rs +++ b/implementations/rust/ockam/ockam_command/src/operation/util.rs @@ -16,7 +16,7 @@ pub async fn check_for_project_completion( node: &InMemoryNode, project: Project, ) -> miette::Result { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(spinner) = pb.as_ref() { let message = format!( "Configuring project...\n{}\n{}", @@ -48,7 +48,7 @@ pub async fn check_for_operation_completion( operation_id: &str, operation_name: &str, ) -> miette::Result<()> { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(spinner) = pb.as_ref() { let message = format!( "Waiting for {operation_name} to finish ...\n{}", diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index 7c92e516d79..9358c57b792 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -134,7 +134,7 @@ impl Command for EnrollCommand { // Issue credential let credential = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Issuing credential..."); } @@ -182,7 +182,7 @@ impl EnrollCommand { enrollment_ticket: EnrollmentTicket, ) -> Result<()> { let enroll_status = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Using enrollment ticket to enroll identity..."); } @@ -231,7 +231,7 @@ impl EnrollCommand { .ok_or(miette!("Okta addon not configured"))? .into(); - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Authenticating with Okta..."); } diff --git a/implementations/rust/ockam/ockam_command/src/project/ticket.rs b/implementations/rust/ockam/ockam_command/src/project/ticket.rs index 25961a2612d..619a5e8248b 100644 --- a/implementations/rust/ockam/ockam_command/src/project/ticket.rs +++ b/implementations/rust/ockam/ockam_command/src/project/ticket.rs @@ -121,7 +121,7 @@ impl Command for TicketCommand { // Request an enrollment token that a future member can use to get a // credential. let token = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Creating an enrollment ticket..."); } diff --git a/implementations/rust/ockam/ockam_command/src/project/util.rs b/implementations/rust/ockam/ockam_command/src/project/util.rs index c39ec6ef315..1894cc6125e 100644 --- a/implementations/rust/ockam/ockam_command/src/project/util.rs +++ b/implementations/rust/ockam/ockam_command/src/project/util.rs @@ -109,7 +109,7 @@ pub async fn check_project_readiness( let retry_strategy = FixedInterval::from_millis(5000) .take((ORCHESTRATOR_AWAIT_TIMEOUT.as_millis() / 5000) as usize); - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); let project = check_project_ready(ctx, node, project, retry_strategy.clone(), pb.clone()).await?; let project = diff --git a/implementations/rust/ockam/ockam_command/src/project_member/delete.rs b/implementations/rust/ockam/ockam_command/src/project_member/delete.rs index 709882fec11..9c9b5de3745 100644 --- a/implementations/rust/ockam/ockam_command/src/project_member/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/project_member/delete.rs @@ -101,7 +101,7 @@ impl Command for DeleteCommand { .filter(|id| id != &self_identifier) .collect::>(); - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = &pb { pb.set_message("Deleting members..."); } diff --git a/implementations/rust/ockam/ockam_command/src/relay/create.rs b/implementations/rust/ockam/ockam_command/src/relay/create.rs index cf7e15e2f33..aad706aa7d7 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/create.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/create.rs @@ -99,7 +99,7 @@ impl Command for CreateCommand { ))?; }; info!("creating a relay at {} to {}", at, node.node_name()); - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating relay at {}...", diff --git a/implementations/rust/ockam/ockam_command/src/relay/list.rs b/implementations/rust/ockam/ockam_command/src/relay/list.rs index 88743e9b1d8..79c539fcae6 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/list.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/list.rs @@ -42,7 +42,7 @@ impl ListCommand { async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { let node = BackgroundNodeClient::create(ctx, &opts.state, &self.to).await?; let relays: Vec = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb { pb.set_message(format!( "Listing Relays on {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/reset.rs b/implementations/rust/ockam/ockam_command/src/reset.rs index 1b4dc6291ee..3b4877b364b 100644 --- a/implementations/rust/ockam/ockam_command/src/reset.rs +++ b/implementations/rust/ockam/ockam_command/src/reset.rs @@ -104,7 +104,7 @@ async fn delete_orchestrator_resources_impl( if spaces.is_empty() { return Ok(()); } - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(s) = pb.as_ref() { s.set_message("Deleting spaces from the Orchestrator..") }; diff --git a/implementations/rust/ockam/ockam_command/src/space/create.rs b/implementations/rust/ockam/ockam_command/src/space/create.rs index 69e7bbce28c..04883fe0e89 100644 --- a/implementations/rust/ockam/ockam_command/src/space/create.rs +++ b/implementations/rust/ockam/ockam_command/src/space/create.rs @@ -52,7 +52,7 @@ impl Command for CreateCommand { let node = InMemoryNode::start(ctx, &opts.state).await?; let space = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Creating a Space for you..."); } @@ -63,7 +63,7 @@ impl Command for CreateCommand { ) .await? }; - if let Ok(msg) = space.subscription_status_message(true) { + if let Ok(msg) = space.subscription_status_message() { opts.terminal.write_line(msg)?; } opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/space/list.rs b/implementations/rust/ockam/ockam_command/src/space/list.rs index f9a59269b34..a676c84275c 100644 --- a/implementations/rust/ockam/ockam_command/src/space/list.rs +++ b/implementations/rust/ockam/ockam_command/src/space/list.rs @@ -32,7 +32,7 @@ impl Command for ListCommand { let node = InMemoryNode::start(ctx, &opts.state).await?; let spaces = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message("Listing spaces..."); } diff --git a/implementations/rust/ockam/ockam_command/src/status.rs b/implementations/rust/ockam/ockam_command/src/status.rs index a38c30f303c..704d3d02d92 100644 --- a/implementations/rust/ockam/ockam_command/src/status.rs +++ b/implementations/rust/ockam/ockam_command/src/status.rs @@ -74,7 +74,7 @@ impl StatusCommand { opts: &CommandGlobalOpts, ) -> Result> { let mut nodes_resources = vec![]; - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); let nodes = opts.state.get_nodes().await?; for node in nodes { if let Some(ref pb) = pb { diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs index a7548a08da0..f8ca201b158 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs @@ -173,7 +173,7 @@ impl Command for CreateCommand { cmd.timeout.timeout.map(|t| node.set_timeout_mut(t)); let inlet_status = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating TCP Inlet at {}...\n", diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs index 97a2fa9ae31..d46d0fc8e3e 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs @@ -36,7 +36,7 @@ impl ListCommand { async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> { let node = BackgroundNodeClient::create(ctx, &opts.state, &self.node.at_node).await?; let inlets: Vec = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!("Listing TCP Inlets on {}...", node.node_name())); } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs index bf4925c07d8..af8ba26aed8 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs @@ -84,7 +84,7 @@ impl Command for CreateCommand { let node = BackgroundNodeClient::create(ctx, &opts.state, &self.at).await?; let node_name = node.node_name(); let outlet_status = { - let pb = opts.terminal.progress_bar(); + let pb = opts.terminal.spinner(); if let Some(pb) = pb.as_ref() { pb.set_message(format!( "Creating a new TCP Outlet to {}...\n",