diff --git a/dozer-api/src/grpc/client_server.rs b/dozer-api/src/grpc/client_server.rs index 6658161c11..0be4405616 100644 --- a/dozer-api/src/grpc/client_server.rs +++ b/dozer-api/src/grpc/client_server.rs @@ -141,7 +141,7 @@ impl ApiServer { } else { unauthenticated_reflection_service = Some(reflection_service); }; - let health_service = auth_middleware.layer(health_service); + let health_service = health_service; let mut auth_service = None; let security = get_api_security(self.security.to_owned()); diff --git a/dozer-cli/src/cli/cloud.rs b/dozer-cli/src/cli/cloud.rs index b84bee9e4b..c22788ce9d 100644 --- a/dozer-cli/src/cli/cloud.rs +++ b/dozer-cli/src/cli/cloud.rs @@ -56,9 +56,6 @@ pub enum CloudCommands { }, /// List all dozer application in Dozer Cloud List(ListCommandArgs), - /// Dozer API server management - #[command(subcommand)] - Api(ApiCommand), /// Dozer app secrets management #[command(subcommand)] Secrets(SecretsCommand), @@ -75,6 +72,9 @@ pub struct DeployCommandArgs { #[arg(long = "allow-incompatible")] pub allow_incompatible: bool, + + #[arg(long = "follow")] + pub follow: bool, } pub fn default_num_api_instances() -> i32 { @@ -144,15 +144,6 @@ pub enum VersionCommand { }, } -#[derive(Debug, Clone, Subcommand)] -pub enum ApiCommand { - /// Sets the number of replicas to serve Dozer APIs - SetNumApiInstances { - /// The number of replicas to set - num_api_instances: i32, - }, -} - #[derive(Debug, Clone, Subcommand)] pub enum SecretsCommand { /// Creates new secret diff --git a/dozer-cli/src/lib.rs b/dozer-cli/src/lib.rs index c42ea4f562..879e328c43 100644 --- a/dozer-cli/src/lib.rs +++ b/dozer-cli/src/lib.rs @@ -21,8 +21,6 @@ pub mod cloud_app_context; mod cloud_helper; pub mod config_helper; pub mod console_helper; -#[cfg(feature = "cloud")] -mod progress_printer; #[cfg(test)] mod tests; mod utils; diff --git a/dozer-cli/src/main.rs b/dozer-cli/src/main.rs index 2a650f69f4..5b7b081312 100644 --- a/dozer-cli/src/main.rs +++ b/dozer-cli/src/main.rs @@ -199,7 +199,6 @@ fn run() -> Result<(), OrchestrationError> { match cloud.command.clone() { CloudCommands::Deploy(deploy) => dozer.deploy(cloud, deploy, cli.config_paths), - CloudCommands::Api(api) => dozer.api(cloud, api), CloudCommands::Login { organisation_slug, profile_name, diff --git a/dozer-cli/src/simple/cloud/deployer.rs b/dozer-cli/src/simple/cloud/deployer.rs index 05e32f9e78..346043c500 100644 --- a/dozer-cli/src/simple/cloud/deployer.rs +++ b/dozer-cli/src/simple/cloud/deployer.rs @@ -1,19 +1,15 @@ use crate::errors::CloudError; +use crate::simple::cloud::progress_printer::ProgressPrinter; use crate::simple::token_layer::TokenLayer; 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::errors::CloudError::GRPCCallError; -use dozer_types::grpc_types::cloud::{Secret, StopRequest, StopResponse}; -use dozer_types::log::{error, info}; - -use crate::cloud_app_context::CloudAppContext; -use crate::progress_printer::ProgressPrinter; -use dozer_types::grpc_types::cloud::AppResponse; use dozer_types::grpc_types::cloud::DeployAppRequest; -use dozer_types::grpc_types::cloud::DeployAppResponse; -use dozer_types::grpc_types::cloud::DeployStep; use dozer_types::grpc_types::cloud::File; +use dozer_types::grpc_types::cloud::{Secret, StopRequest, StopResponse}; +use dozer_types::log::info; pub async fn deploy_app( client: &mut DozerCloudClient, @@ -21,8 +17,9 @@ pub async fn deploy_app( secrets: Vec, allow_incompatible: bool, files: Vec, + follow: bool, ) -> Result<(), CloudError> { - let mut response = client + let response = client .deploy_application(DeployAppRequest { app_id: app_id.clone(), secrets, @@ -32,55 +29,65 @@ pub async fn deploy_app( .await? .into_inner(); + let app_id = response.app_id; + let deployment_id = response.deployment_id; + let url = response.deployment_url; + info!("Deploying new application with App Id: {app_id}, Deployment Id: {deployment_id}"); + info!("Follow the deployment progress at {url}"); + + if follow { + print_progress(client, app_id, deployment_id).await?; + } + + Ok::<(), CloudError>(()) +} + +async fn print_progress( + client: &mut DozerCloudClient, + app_id: String, + deployment_id: String, +) -> Result<(), CloudError> { + let mut current_step = 0; let mut printer = ProgressPrinter::new(); - while let Some(DeployAppResponse { - result, - completed: _, - error: error_message, - }) = response.message().await.map_err(GRPCCallError)? - { - if let Some(message) = error_message { - error!("{}", message); + let request = GetDeploymentStatusRequest { + app_id, + deployment_id, + }; + loop { + let response = client + .get_deployment_status(request.clone()) + .await? + .into_inner(); + + if response.status == DeploymentStatus::Success as i32 { + info!("Deployment completed successfully"); + break; + } else if response.status == DeploymentStatus::Failed as i32 { + info!("Deployment failed!"); + break; } else { - match result { - Some(dozer_types::grpc_types::cloud::deploy_app_response::Result::AppCreated( - AppResponse { - app_id: new_app_id, .. - }, - )) => { - if let Some(app_id) = app_id { - info!("Updating {app_id} application"); - } else { - info!("Deploying new application {new_app_id}"); - CloudAppContext::save_app_id(new_app_id)?; - } - } - Some(dozer_types::grpc_types::cloud::deploy_app_response::Result::Step( - DeployStep { - text, - is_completed, - current_step, - .. - }, - )) => { - if is_completed { - printer.complete_step(current_step, &text); - } else { - printer.start_step(current_step, &text); - } - } - Some(dozer_types::grpc_types::cloud::deploy_app_response::Result::LogMessage( - message, - )) => { - message.split('\n').for_each(|line| { - info!("{}", line); - }); - } - None => {} + let steps = response.steps.clone(); + let completed_steps = response + .steps + .into_iter() + .filter(|s| { + s.status == DeploymentStatus::Success as i32 && s.step_index >= current_step + }) + .map(|s| (s.step_index, s.step_text)) + .collect::>(); + + for (step_no, text) in completed_steps.iter() { + printer.complete_step(*step_no, text) + } + + if !completed_steps.is_empty() { + current_step = completed_steps.last().unwrap().0 + 1; + let text = steps[current_step as usize].step_text.clone(); + 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/mod.rs b/dozer-cli/src/simple/cloud/mod.rs index ef2fb20cf0..b76b2213a2 100644 --- a/dozer-cli/src/simple/cloud/mod.rs +++ b/dozer-cli/src/simple/cloud/mod.rs @@ -1,4 +1,5 @@ pub mod deployer; pub mod login; pub mod monitor; +pub mod progress_printer; pub mod version; diff --git a/dozer-cli/src/progress_printer.rs b/dozer-cli/src/simple/cloud/progress_printer.rs similarity index 100% rename from dozer-cli/src/progress_printer.rs rename to dozer-cli/src/simple/cloud/progress_printer.rs diff --git a/dozer-cli/src/simple/cloud_orchestrator.rs b/dozer-cli/src/simple/cloud_orchestrator.rs index 8afab25639..d2f71a6236 100644 --- a/dozer-cli/src/simple/cloud_orchestrator.rs +++ b/dozer-cli/src/simple/cloud_orchestrator.rs @@ -1,6 +1,5 @@ use crate::cli::cloud::{ - ApiCommand, Cloud, DeployCommandArgs, ListCommandArgs, LogCommandArgs, SecretsCommand, - VersionCommand, + Cloud, DeployCommandArgs, ListCommandArgs, LogCommandArgs, SecretsCommand, VersionCommand, }; use crate::cloud_app_context::CloudAppContext; use crate::cloud_helper::list_files; @@ -20,8 +19,8 @@ use dozer_types::grpc_types::cloud::{ LogMessageRequest, UpdateSecretRequest, }; use dozer_types::grpc_types::cloud::{ - DeploymentStatus, File, SetCurrentVersionRequest, SetNumApiInstancesRequest, - UpsertVersionRequest, + DeploymentInfo, DeploymentStatusWithHealth, File, ListDeploymentRequest, + SetCurrentVersionRequest, UpsertVersionRequest, }; use dozer_types::log::info; use dozer_types::prettytable::{row, table}; @@ -104,6 +103,7 @@ impl CloudOrchestrator for SimpleOrchestrator { deploy.secrets, deploy.allow_incompatible, files, + deploy.follow, ) .await?; Ok::<(), OrchestrationError>(()) @@ -229,7 +229,7 @@ impl CloudOrchestrator for SimpleOrchestrator { let mut table = table!(); - table.add_row(row!["Api endpoint", response.api_endpoint,]); + table.add_row(row!["Api endpoint", response.data_endpoint,]); let mut deployment_table = table!(); deployment_table.set_titles(row![ @@ -242,8 +242,7 @@ impl CloudOrchestrator for SimpleOrchestrator { ]); for status in response.deployments.iter() { - let deployment = status.deployment; - + let deployment = status.deployment.as_ref().expect("deployment is expected"); fn mark(status: bool) -> &'static str { if status { "🟢" @@ -252,17 +251,9 @@ impl CloudOrchestrator for SimpleOrchestrator { } } - fn number(number: Option) -> String { - if let Some(n) = number { - n.to_string() - } else { - "-".to_string() - } - } - let mut version = "".to_string(); for (loop_version, loop_deployment) in response.versions.iter() { - if loop_deployment == &deployment { + if loop_deployment == &deployment.deployment { if Some(*loop_version) == response.current_version { version = format!("v{loop_version} (current)"); } else { @@ -273,17 +264,17 @@ impl CloudOrchestrator for SimpleOrchestrator { } deployment_table.add_row(row![ - deployment, - mark(status.app_running), - format!( - "{}/{}", - number(status.api_available), - number(status.api_desired) - ), - version, - status.phase, - status.last_error.as_deref().unwrap_or("None") + deployment.deployment, + format!("Deployment Status: {:?}", deployment.status), + format!("Version: {}", version), ]); + for r in status.resources.iter() { + deployment_table.add_row(row![ + "", + format!("{}: {}", r.typ, mark(r.available == r.desired)), + format!("{}/{}", r.available.unwrap_or(0), r.desired.unwrap_or(0)), + ]); + } } table.add_row(row!["Deployments", deployment_table]); @@ -309,8 +300,8 @@ impl CloudOrchestrator for SimpleOrchestrator { self.runtime.block_on(async move { let mut client = get_cloud_client(&cloud, cloud_config).await?; - let status = client - .get_status(GetStatusRequest { + let res = client + .list_deployments(ListDeploymentRequest { app_id: app_id.clone(), }) .await @@ -320,7 +311,7 @@ impl CloudOrchestrator for SimpleOrchestrator { // Show log of the latest deployment for now. let Some(deployment) = logs .deployment - .or_else(|| latest_deployment(&status.deployments)) + .or_else(|| latest_deployment(&res.deployments)) else { info!("No deployments found"); return Ok(()); @@ -532,7 +523,7 @@ impl SimpleOrchestrator { let api_available = get_api_available(&status.deployments, *deployment); let version_status = - get_version_status(&status.api_endpoint, version, api_available).await; + get_version_status(&status.data_endpoint, version, api_available).await; let mut table = table!(); if let Some(current_version) = status.current_version { @@ -548,7 +539,7 @@ impl SimpleOrchestrator { ]); let current_version_status = get_version_status( - &status.api_endpoint, + &status.data_endpoint, current_version, current_api_available, ) @@ -587,50 +578,29 @@ impl SimpleOrchestrator { })?; Ok(()) } - - pub fn api(&self, cloud: Cloud, api: ApiCommand) -> Result<(), OrchestrationError> { - self.runtime.block_on(async move { - let app_id = CloudAppContext::get_app_id(self.config.cloud.as_ref())?; - - let mut client = get_cloud_client(&cloud, self.config.cloud.as_ref()).await?; - - match api { - ApiCommand::SetNumApiInstances { num_api_instances } => { - let status = client - .get_status(GetStatusRequest { - app_id: app_id.clone(), - }) - .await? - .into_inner(); - // Update the latest deployment for now. - let Some(deployment) = latest_deployment(&status.deployments) else { - info!("No deployments found"); - return Ok(()); - }; - client - .set_num_api_instances(SetNumApiInstancesRequest { - app_id, - deployment, - num_api_instances, - }) - .await?; - } - } - Ok::<_, CloudError>(()) - })?; - Ok(()) - } } -fn latest_deployment(deployments: &[DeploymentStatus]) -> Option { +fn latest_deployment(deployments: &[DeploymentInfo]) -> Option { deployments.iter().map(|status| status.deployment).max() } -fn get_api_available(deployments: &[DeploymentStatus], deployment: u32) -> i32 { - deployments +fn get_api_available(deployments: &[DeploymentStatusWithHealth], deployment: u32) -> i32 { + let info = deployments .iter() - .find(|status| status.deployment == deployment) - .expect("Deployment should be found in deployments") - .api_available + .find(|status| { + status + .deployment + .as_ref() + .expect("deployment is expected") + .deployment + == deployment + }) + .expect("Deployment should be found in deployments"); + + info.resources + .clone() + .into_iter() + .find(|r| r.typ == "api") + .and_then(|r| r.available) .unwrap_or(1) } diff --git a/dozer-types/protos/cloud.proto b/dozer-types/protos/cloud.proto index 132dc87fed..d27ca747ff 100644 --- a/dozer-types/protos/cloud.proto +++ b/dozer-types/protos/cloud.proto @@ -6,33 +6,35 @@ import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; service DozerCloud { - rpc create_application(CreateAppRequest) returns (AppResponse); - rpc DeployApplication(DeployAppRequest) returns (stream DeployAppResponse); - rpc update_application(UpdateAppRequest) returns (AppResponse); - rpc delete_application(DeleteAppRequest) returns (DeleteAppResponse); - rpc list_applications(ListAppRequest) returns (ListAppResponse); - rpc get_connection(GetConnectionRequest) returns (ConnectionResponse); - - rpc get_application(GetAppRequest) returns (AppResponse); + rpc validate_connection(ConnectionRequest) returns (ValidateConnectionResponse); rpc create_connection(ConnectionRequest) returns (ConnectionResponse); + rpc get_connection(GetConnectionRequest) returns (ConnectionResponse); rpc list_connections(GetAllConnectionRequest) returns (GetAllConnectionResponse); rpc get_tables(GetTablesRequest) returns (GetTablesResponse); rpc update_connection(UpdateConnectionRequest) returns (ConnectionResponse); - rpc StartDozer(StartRequest) returns (stream StartUpdate); + rpc list_applications(ListAppRequest) returns (ListAppResponse); + + // Application + rpc create_application(CreateAppRequest) returns (AppResponse); + rpc deploy_application(DeployAppRequest) returns (DeployAppResponse); + rpc update_application(UpdateAppRequest) returns (AppResponse); + rpc delete_application(DeleteAppRequest) returns (DeleteAppResponse); + rpc get_application(GetAppRequest) returns (AppResponse); rpc stop_dozer(StopRequest) returns (StopResponse); rpc get_status(GetStatusRequest) returns (GetStatusResponse); + rpc list_deployments(ListDeploymentRequest) returns (ListDeploymentResponse); + rpc list_versions(ListVersionsRequest) returns (ListVersionsResponse); rpc upsert_version(UpsertVersionRequest) returns (UpsertVersionResponse); rpc set_current_version(SetCurrentVersionRequest) returns (SetCurrentVersionResponse); - rpc set_num_api_instances(SetNumApiInstancesRequest) returns (SetNumApiInstancesResponse); - - rpc list_deployments(ListDeploymentRequest) returns (ListDeploymentResponse); - rpc list_files(ListFilesRequest) returns (ListFilesResponse); + // returns a list of all the steps for a deployment + rpc get_deployment_status(GetDeploymentStatusRequest) returns (GetDeploymentStatusResponse); + rpc create_secret(CreateSecretRequest) returns (CreateSecretResponse); rpc update_secret(UpdateSecretRequest) returns (UpdateSecretResponse); rpc delete_secret(DeleteSecretRequest) returns (DeleteSecretResponse); @@ -60,27 +62,15 @@ message DeployAppRequest { bool allow_incompatible = 4; } -message DeployStep { - bool is_completed = 1; - string text = 2; - uint32 current_step = 3; - optional string error = 4; -} - message DeployAppResponse { - oneof result { - AppResponse app_created = 1; - DeployStep step = 2; - string log_message = 4; - } - bool completed = 5; - optional string error = 6; + string app_id = 1; + string deployment_id = 2; + string deployment_url = 3; } message AppResponse { string app_id = 1; AppConfig app = 2; - optional string repository_url = 3; repeated DeploymentInfo deployments = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; @@ -111,12 +101,20 @@ message DeleteAppResponse { bool success = 1; } message GetAppRequest { optional string app_id = 1; } -message AppHealthRequest { +message GetStatusRequest{ string app_id = 1; } -message AppHealthResponse { - bool healthy = 1; +message GetStatusResponse { + string data_endpoint = 1; + repeated DeploymentStatusWithHealth deployments = 2; + map versions = 4; + optional uint32 current_version = 5; } +message DeploymentStatusWithHealth { + DeploymentInfo deployment = 1; + repeated DeploymentResource resources = 2; +} + message GetResourcesRequest { string app_id = 1; // if not specified, will get latest version @@ -124,8 +122,6 @@ message GetResourcesRequest { } message ResourcesResponse { repeated DeploymentResource resources = 1; - map versions = 3; - uint32 current_version = 4; } message DeploymentResource { @@ -141,42 +137,46 @@ message DeploymentResource { optional int32 unavailable = 6; } -message StartRequest { - string app_id = 1; - repeated Secret secrets = 3; - bool allow_incompatible = 4; +message StopRequest { string app_id = 1; } +message StopResponse { bool success = 1; } + +enum DeploymentStatus { + PENDING = 0; + RUNNING = 1; + SUCCESS = 2; + FAILED = 3; } -message StartResponse { - bool success = 1; - string app_id = 2; - string api_endpoint = 3; - optional string error = 4; +message ListVersionsRequest { string app_id = 1; } +message ListVersionsResponse { + optional uint32 current_version = 4; + map versions = 3; } -message StartUpdate { - optional StartResponse result = 1; - optional uint32 current_step = 2; - optional uint32 total_steps = 3; - optional LogMessage last_message = 4; + +message DeploymentInfo { + string deployment_id = 1; + uint32 deployment = 2; + DeploymentStatus status = 3; + optional google.protobuf.Timestamp created_at = 4; + optional google.protobuf.Timestamp updated_at = 5; } -message StopRequest { string app_id = 1; } -message StopResponse { bool success = 1; } -message GetStatusRequest { string app_id = 1; } -message GetStatusResponse { - string api_endpoint = 1; - repeated DeploymentStatus deployments = 2; - map versions = 3; - optional uint32 current_version = 4; +message GetDeploymentStatusRequest { + string app_id = 1; + string deployment_id = 2; +} +message GetDeploymentStatusResponse { + DeploymentStatus status = 1; + repeated DeploymentStep steps = 2; + optional google.protobuf.Timestamp created_at = 5; + optional google.protobuf.Timestamp updated_at = 6; } -message DeploymentStatus { - uint32 deployment = 1; - bool app_running = 2; - optional int32 api_desired = 3; - optional int32 api_available = 4; - string phase = 5; - optional string last_error = 6; +message DeploymentStep { + uint32 step_index = 1; + string step_text = 2; + DeploymentStatus status = 3; + string logs = 4; } message ListDeploymentRequest { @@ -184,8 +184,7 @@ message ListDeploymentRequest { } message ListDeploymentResponse { - string app_id = 1; - repeated int32 deployment = 2; + repeated DeploymentInfo deployments = 2; } message UpsertVersionRequest { @@ -203,25 +202,6 @@ message SetCurrentVersionRequest { message SetCurrentVersionResponse {} -message SetNumApiInstancesRequest { - string app_id = 1; - uint32 deployment = 2; - int32 num_api_instances = 3; -} - -message SetNumApiInstancesResponse {} - - - -message DeploymentInfo { - uint32 deployment = 1; - string phase = 2; - optional string last_error = 3; - optional google.protobuf.Timestamp created_at = 4; - optional google.protobuf.Timestamp updated_at = 5; -} - - message ConnectionRequest { Connection connection = 1; } message GetConnectionRequest { string connection_id = 1;