diff --git a/dozer-cli/src/cli/cloud.rs b/dozer-cli/src/cli/cloud.rs index c22788ce9d..83a7e0c2ea 100644 --- a/dozer-cli/src/cli/cloud.rs +++ b/dozer-cli/src/cli/cloud.rs @@ -59,6 +59,12 @@ pub enum CloudCommands { /// Dozer app secrets management #[command(subcommand)] Secrets(SecretsCommand), + /// Get example of API call + #[command(name = "api-request-samples")] + ApiRequestSamples { + #[arg(long, short)] + endpoint: Option, + }, } #[derive(Debug, Args, Clone)] diff --git a/dozer-cli/src/errors.rs b/dozer-cli/src/errors.rs index 740a640f7a..c4d7eef40a 100644 --- a/dozer-cli/src/errors.rs +++ b/dozer-cli/src/errors.rs @@ -52,7 +52,7 @@ pub enum OrchestrationError { GenerateTokenFailed(#[source] AuthError), #[error("Missing api config or security input")] MissingSecurityConfig, - #[error("Cloud service error: {0}")] + #[error(transparent)] CloudError(#[from] CloudError), #[error("Failed to initialize api server: {0}")] ApiInitFailed(#[from] ApiInitError), @@ -142,7 +142,7 @@ pub enum CloudError { #[error("Connection failed. Error: {0:?}")] ConnectionToCloudServiceError(#[from] tonic::transport::Error), - #[error("Cloud service returned error: {0:?}")] + #[error("Cloud service returned error: {}", .0.message())] CloudServiceError(#[from] tonic::Status), #[error("GRPC request failed, error: {} (GRPC status {})", .0.message(), .0.code())] diff --git a/dozer-cli/src/main.rs b/dozer-cli/src/main.rs index 5b7b081312..2e94f4b0bd 100644 --- a/dozer-cli/src/main.rs +++ b/dozer-cli/src/main.rs @@ -223,6 +223,9 @@ fn run() -> Result<(), OrchestrationError> { info!("Using \"{app_id}\" app"); Ok(()) } + CloudCommands::ApiRequestSamples { endpoint } => { + dozer.print_api_request_samples(cloud, endpoint) + } } } Commands::Init => { diff --git a/dozer-cli/src/simple/cloud/deployer.rs b/dozer-cli/src/simple/cloud/deployer.rs index 346043c500..32512561f2 100644 --- a/dozer-cli/src/simple/cloud/deployer.rs +++ b/dozer-cli/src/simple/cloud/deployer.rs @@ -6,10 +6,11 @@ use dozer_types::grpc_types::cloud::dozer_cloud_client::DozerCloudClient; use dozer_types::grpc_types::cloud::DeploymentStatus; use dozer_types::grpc_types::cloud::GetDeploymentStatusRequest; +use crate::cloud_app_context::CloudAppContext; use dozer_types::grpc_types::cloud::DeployAppRequest; use dozer_types::grpc_types::cloud::File; use dozer_types::grpc_types::cloud::{Secret, StopRequest, StopResponse}; -use dozer_types::log::info; +use dozer_types::log::{info, warn}; pub async fn deploy_app( client: &mut DozerCloudClient, @@ -50,7 +51,7 @@ async fn print_progress( let mut current_step = 0; let mut printer = ProgressPrinter::new(); let request = GetDeploymentStatusRequest { - app_id, + app_id: app_id.clone(), deployment_id, }; loop { @@ -61,9 +62,13 @@ async fn print_progress( if response.status == DeploymentStatus::Success as i32 { info!("Deployment completed successfully"); + info!("You can get API requests samples with `dozer cloud api-request-samples`"); + + CloudAppContext::save_app_id(app_id)?; + break; } else if response.status == DeploymentStatus::Failed as i32 { - info!("Deployment failed!"); + warn!("Deployment failed!"); break; } else { let steps = response.steps.clone(); @@ -86,6 +91,7 @@ async fn print_progress( printer.start_step(current_step, &text); } } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; } Ok::<(), CloudError>(()) diff --git a/dozer-cli/src/simple/cloud_orchestrator.rs b/dozer-cli/src/simple/cloud_orchestrator.rs index 4eff819ab9..52fa85c436 100644 --- a/dozer-cli/src/simple/cloud_orchestrator.rs +++ b/dozer-cli/src/simple/cloud_orchestrator.rs @@ -4,6 +4,7 @@ use crate::cli::cloud::{ use crate::cloud_app_context::CloudAppContext; use crate::cloud_helper::list_files; +use crate::console_helper::{get_colored_text, PURPLE}; use crate::errors::OrchestrationError::FailedToReadOrganisationName; use crate::errors::{map_tonic_error, CliError, CloudError, CloudLoginError, OrchestrationError}; use crate::simple::cloud::deployer::{deploy_app, stop_app}; @@ -13,10 +14,12 @@ use crate::simple::token_layer::TokenLayer; use crate::simple::SimpleOrchestrator; use crate::CloudOrchestrator; use dozer_types::constants::{DEFAULT_CLOUD_TARGET_URL, LOCK_FILE}; +use dozer_types::grpc_types::api_explorer::api_explorer_service_client::ApiExplorerServiceClient; +use dozer_types::grpc_types::api_explorer::GetApiTokenRequest; use dozer_types::grpc_types::cloud::{ dozer_cloud_client::DozerCloudClient, CreateSecretRequest, DeleteAppRequest, - DeleteSecretRequest, GetSecretRequest, GetStatusRequest, ListAppRequest, ListSecretsRequest, - LogMessageRequest, UpdateSecretRequest, + DeleteSecretRequest, GetEndpointCommandsSamplesRequest, GetSecretRequest, GetStatusRequest, + ListAppRequest, ListSecretsRequest, LogMessageRequest, UpdateSecretRequest, }; use dozer_types::grpc_types::cloud::{ DeploymentInfo, DeploymentStatusWithHealth, File, ListDeploymentRequest, @@ -56,6 +59,30 @@ pub async fn get_cloud_client( Ok(client) } +pub async fn get_explorer_client( + cloud: &Cloud, + cloud_config: Option<&dozer_types::models::cloud::Cloud>, +) -> Result, CloudError> { + let profile_name = match &cloud.profile { + None => cloud_config.as_ref().and_then(|c| c.profile.clone()), + Some(_) => cloud.profile.clone(), + }; + let credential = CredentialInfo::load(profile_name)?; + let target_url = cloud + .target_url + .as_ref() + .unwrap_or(&credential.target_url) + .clone(); + + let endpoint = Endpoint::from_shared(target_url.to_owned())?; + let channel = Endpoint::connect(&endpoint).await?; + let channel = ServiceBuilder::new() + .layer_fn(|channel| TokenLayer::new(channel, credential.clone())) + .service(channel); + let client = ApiExplorerServiceClient::new(channel); + Ok(client) +} + impl CloudOrchestrator for SimpleOrchestrator { // TODO: Deploy Dozer application using local Dozer configuration fn deploy( @@ -78,6 +105,7 @@ impl CloudOrchestrator for SimpleOrchestrator { let lockfile_path = self.lockfile_path(); self.runtime.block_on(async move { let mut client = get_cloud_client(&cloud, cloud_config).await?; + // let mut explorer_client = get_explorer_client(&cloud, cloud_config).await?; let mut files = list_files(config_paths)?; if deploy.locked { let lockfile_contents = tokio::fs::read_to_string(lockfile_path) @@ -99,6 +127,7 @@ impl CloudOrchestrator for SimpleOrchestrator { // 2. START application deploy_app( &mut client, + // &mut explorer_client, &app_id, deploy.secrets, deploy.allow_incompatible, @@ -576,6 +605,65 @@ impl SimpleOrchestrator { })?; Ok(()) } + + pub fn print_api_request_samples( + &self, + cloud: Cloud, + endpoint: Option, + ) -> Result<(), OrchestrationError> { + let app_id = cloud + .app_id + .clone() + .unwrap_or(CloudAppContext::get_app_id(self.config.cloud.as_ref())?); + let cloud_config = self.config.cloud.as_ref(); + self.runtime.block_on(async move { + let mut client = get_cloud_client(&cloud, cloud_config).await?; + let mut explorer_client = get_explorer_client(&cloud, cloud_config).await?; + + let response = client + .get_endpoint_commands_samples(GetEndpointCommandsSamplesRequest { + app_id: app_id.clone(), + endpoint, + }) + .await + .map_err(map_tonic_error)? + .into_inner(); + + let token_response = explorer_client + .get_api_token(GetApiTokenRequest { + app_id: Some(app_id), + ttl: Some(3600), + }) + .await? + .into_inner(); + + let mut rows = vec![]; + let token = match token_response.token { + Some(token) => token, + None => { + info!("Replace $DOZER_TOKEN with your API authorization token"); + "$DOZER_TOKEN".to_string() + } + }; + + for sample in response.samples { + rows.push(get_colored_text( + &format!( + "\n##################### {} command ###########################\n", + sample.r#type + ), + PURPLE, + )); + rows.push(sample.command.replace("{token}", &token).to_string()); + } + + info!("{}", rows.join("\n")); + + Ok::<(), CloudError>(()) + })?; + + Ok(()) + } } fn latest_deployment(deployments: &[DeploymentInfo]) -> Option { diff --git a/dozer-types/protos/cloud.proto b/dozer-types/protos/cloud.proto index 3026e204d9..5174d68772 100644 --- a/dozer-types/protos/cloud.proto +++ b/dozer-types/protos/cloud.proto @@ -50,6 +50,8 @@ service DozerCloud { rpc list_notifications(ListNotificationsRequest) returns (ListNotificationsResponse); rpc mark_notifications_as_read(MarkNotificationsRequest) returns (MarkNotificationsResponse); + + rpc get_endpoint_commands_samples(GetEndpointCommandsSamplesRequest) returns (GetEndpointCommandsSamplesResponse); } service DozerPublic { @@ -352,3 +354,18 @@ message ListSecretsRequest { message ListSecretsResponse { repeated string secrets = 1; } + +message CommandSample { + string type = 1; + string command = 2; + optional string description = 3; +} + +message GetEndpointCommandsSamplesRequest { + string app_id = 1; + optional string endpoint = 2; +} + +message GetEndpointCommandsSamplesResponse { + repeated CommandSample samples = 1; +}