diff --git a/Cargo.lock b/Cargo.lock index c869e30..44372a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,6 +578,7 @@ dependencies = [ "futures-util", "golem-client", "golem-examples", + "golem-gateway-client", "http", "indoc", "itertools", @@ -632,6 +633,20 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "golem-gateway-client" +version = "0.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5844ff15ead450aa41f8f267f63c959aba36474453b08249ed1ffc40ee6c95a9" +dependencies = [ + "reqwest", + "serde", + "serde_derive", + "serde_json", + "url", + "uuid", +] + [[package]] name = "h2" version = "0.3.21" @@ -1816,6 +1831,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ + "getrandom", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 68b91dd..4140b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ clap-verbosity-flag = "2.0.1" derive_more = "0.99.17" futures-util = "0.3.28" golem-client = "0.0.47" +golem-gateway-client = "0.0.47" golem-examples = "0.1.8" http = "0.2.9" indoc = "2.0.4" diff --git a/src/clients.rs b/src/clients.rs index 3782748..87d80b8 100644 --- a/src/clients.rs +++ b/src/clients.rs @@ -3,6 +3,7 @@ use golem_client::model::{TokenSecret, UnsafeToken}; use crate::model::{AccountId, ProjectAction}; pub mod account; +pub mod gateway; pub mod grant; pub mod login; pub mod policy; diff --git a/src/clients/gateway.rs b/src/clients/gateway.rs new file mode 100644 index 0000000..7023322 --- /dev/null +++ b/src/clients/gateway.rs @@ -0,0 +1,6 @@ +pub mod deployment; +pub mod errors; +pub mod healthcheck; +pub mod definition; +pub mod certificate; +pub mod domain; diff --git a/src/clients/gateway/certificate.rs b/src/clients/gateway/certificate.rs new file mode 100644 index 0000000..9569dcd --- /dev/null +++ b/src/clients/gateway/certificate.rs @@ -0,0 +1,49 @@ +use async_trait::async_trait; +use golem_gateway_client::apis::api_certificate_api::{v1_api_certificates_delete, v1_api_certificates_get, v1_api_certificates_post}; +use golem_gateway_client::apis::configuration::Configuration; +use golem_gateway_client::models::{Certificate, CertificateRequest}; +use tracing::info; + +use crate::model::{GolemError, ProjectId}; + +#[async_trait] +pub trait CertificateClient { + async fn get( + &self, + project_id: ProjectId, + certificate_id: Option<&str>, + ) -> Result, GolemError>; + + async fn create( + &self, + certificate: CertificateRequest, + ) -> Result; + + async fn delete( + &self, + project_id: ProjectId, + certificate_id: &str, + ) -> Result; +} + +pub struct CertificateClientLive { + pub configuration: Configuration, +} + +#[async_trait] +impl CertificateClient for CertificateClientLive { + async fn get(&self, project_id: ProjectId, certificate_id: Option<&str>) -> Result, GolemError> { + info!("Calling v1_api_certificates_get for project_id {project_id:?}, certificate_id {certificate_id:?} on base url {}", self.configuration.base_path); + Ok(v1_api_certificates_get(&self.configuration, &project_id.0.to_string(), certificate_id).await?) + } + + async fn create(&self, certificate: CertificateRequest) -> Result { + info!("Calling v1_api_certificates_post on base url {}", self.configuration.base_path); + Ok(v1_api_certificates_post(&self.configuration, certificate).await?) + } + + async fn delete(&self, project_id: ProjectId, certificate_id: &str) -> Result { + info!("Calling v1_api_certificates_delete for project_id {project_id:?}, certificate_id {certificate_id} on base url {}", self.configuration.base_path); + Ok(v1_api_certificates_delete(&self.configuration, &project_id.0.to_string(), certificate_id).await?) + } +} \ No newline at end of file diff --git a/src/clients/gateway/definition.rs b/src/clients/gateway/definition.rs new file mode 100644 index 0000000..1c4a8b9 --- /dev/null +++ b/src/clients/gateway/definition.rs @@ -0,0 +1,49 @@ +use async_trait::async_trait; +use golem_gateway_client::apis::api_definition_api::{v1_api_definitions_delete, v1_api_definitions_get, v1_api_definitions_put}; +use golem_gateway_client::apis::configuration::Configuration; +use golem_gateway_client::models::ApiDefinition; +use tracing::info; + +use crate::model::{GolemError, ProjectId}; + +#[async_trait] +pub trait DefinitionClient { + async fn get( + &self, + project_id: ProjectId, + api_definition_id: Option<&str>, + ) -> Result, GolemError>; + + async fn update( + &self, + api_definition: ApiDefinition, + ) -> Result; + + async fn delete( + &self, + project_id: ProjectId, + api_definition_id: &str, + ) -> Result; +} + +pub struct DefinitionClientLive { + pub configuration: Configuration, +} + +#[async_trait] +impl DefinitionClient for DefinitionClientLive { + async fn get(&self, project_id: ProjectId, api_definition_id: Option<&str>) -> Result, GolemError> { + info!("Calling v1_api_definitions_get for project_id {project_id:?}, api_definition_id {api_definition_id:?} on base url {}", self.configuration.base_path); + Ok(v1_api_definitions_get(&self.configuration, &project_id.0.to_string(), api_definition_id).await?) + } + + async fn update(&self, api_definition: ApiDefinition) -> Result { + info!("Calling v1_api_definitions_put on base url {}", self.configuration.base_path); + Ok(v1_api_definitions_put(&self.configuration, api_definition).await?) + } + + async fn delete(&self, project_id: ProjectId, api_definition_id: &str) -> Result { + info!("Calling v1_api_definitions_delete for project_id {project_id:?}, api_definition_id {api_definition_id} on base url {}", self.configuration.base_path); + Ok(v1_api_definitions_delete(&self.configuration, &project_id.0.to_string(), api_definition_id).await?) + } +} \ No newline at end of file diff --git a/src/clients/gateway/deployment.rs b/src/clients/gateway/deployment.rs new file mode 100644 index 0000000..6d0531b --- /dev/null +++ b/src/clients/gateway/deployment.rs @@ -0,0 +1,70 @@ +use async_trait::async_trait; +use golem_gateway_client::apis::api_deployment_api::{ + v1_api_deployments_delete, v1_api_deployments_get, v1_api_deployments_put, +}; +use golem_gateway_client::apis::configuration::Configuration; +use golem_gateway_client::models::ApiDeployment; +use tracing::info; + +use crate::model::{GolemError, ProjectId}; + +#[async_trait] +pub trait DeploymentClient { + async fn get( + &self, + project_id: ProjectId, + api_definition_id: &str, + ) -> Result, GolemError>; + async fn update(&self, api_deployment: ApiDeployment) -> Result; + async fn delete( + &self, + project_id: ProjectId, + api_definition_id: &str, + site: &str, + ) -> Result; +} + +pub struct DeploymentClientLive { + pub configuration: Configuration, +} + +#[async_trait] +impl DeploymentClient for DeploymentClientLive { + async fn get( + &self, + project_id: ProjectId, + api_definition_id: &str, + ) -> Result, GolemError> { + info!("Calling v1_api_deployments_get for project_id {project_id:?}, api_definition_id {api_definition_id} on base url: {}", self.configuration.base_path); + Ok(v1_api_deployments_get( + &self.configuration, + &project_id.0.to_string(), + api_definition_id, + ) + .await?) + } + + async fn update(&self, api_deployment: ApiDeployment) -> Result { + info!( + "Calling v1_api_deployments_put on base url: {}", + self.configuration.base_path + ); + Ok(v1_api_deployments_put(&self.configuration, api_deployment).await?) + } + + async fn delete( + &self, + project_id: ProjectId, + api_definition_id: &str, + site: &str, + ) -> Result { + info!("Calling v1_api_deployments_delete for project_id {project_id:?}, api_definition_id {api_definition_id}, site {site} on base url: {}", self.configuration.base_path); + Ok(v1_api_deployments_delete( + &self.configuration, + &project_id.0.to_string(), + api_definition_id, + site, + ) + .await?) + } +} diff --git a/src/clients/gateway/domain.rs b/src/clients/gateway/domain.rs new file mode 100644 index 0000000..a1c4138 --- /dev/null +++ b/src/clients/gateway/domain.rs @@ -0,0 +1,49 @@ +use async_trait::async_trait; +use golem_gateway_client::apis::api_domain_api::{v1_api_domains_delete, v1_api_domains_get, v1_api_domains_put}; +use golem_gateway_client::apis::configuration::Configuration; +use golem_gateway_client::models::{ApiDomain, DomainRequest}; +use tracing::info; + +use crate::model::{GolemError, ProjectId}; + +#[async_trait] +pub trait DomainClient { + async fn get( + &self, + project_id: ProjectId, + ) -> Result, GolemError>; + + async fn update( + &self, + project_id: ProjectId, + domain_name: String, + ) -> Result; + + async fn delete( + &self, + project_id: ProjectId, + domain_name: &str, + ) -> Result; +} + +pub struct DomainClientLive { + pub configuration: Configuration, +} + +#[async_trait] +impl DomainClient for DomainClientLive { + async fn get(&self, project_id: ProjectId) -> Result, GolemError> { + info!("Calling v1_api_domains_get for project_id {project_id:?} on base url {}", self.configuration.base_path); + Ok(v1_api_domains_get(&self.configuration, &project_id.0.to_string()).await?) + } + + async fn update(&self, project_id: ProjectId, domain_name: String) -> Result { + info!("Calling v1_api_domains_get for project_id {project_id:?}, domain_name {domain_name} on base url {}", self.configuration.base_path); + Ok(v1_api_domains_put(&self.configuration, DomainRequest{project_id: project_id.0, domain_name}).await?) + } + + async fn delete(&self, project_id: ProjectId, domain_name: &str) -> Result { + info!("Calling v1_api_domains_get for project_id {project_id:?}, domain_name {domain_name} on base url {}", self.configuration.base_path); + Ok(v1_api_domains_delete(&self.configuration, &project_id.0.to_string(), domain_name).await?) + } +} \ No newline at end of file diff --git a/src/clients/gateway/errors.rs b/src/clients/gateway/errors.rs new file mode 100644 index 0000000..4dc7ba7 --- /dev/null +++ b/src/clients/gateway/errors.rs @@ -0,0 +1,367 @@ +use golem_gateway_client::apis::api_certificate_api::{V1ApiCertificatesDeleteError, V1ApiCertificatesGetError, V1ApiCertificatesPostError}; +use golem_gateway_client::apis::api_definition_api::{V1ApiDefinitionsDeleteError, V1ApiDefinitionsGetError, V1ApiDefinitionsPutError}; +use golem_gateway_client::apis::api_deployment_api::{ + V1ApiDeploymentsDeleteError, V1ApiDeploymentsGetError, V1ApiDeploymentsPutError, +}; +use golem_gateway_client::apis::api_domain_api::{V1ApiDomainsDeleteError, V1ApiDomainsGetError, V1ApiDomainsPutError}; +use golem_gateway_client::apis::healthcheck_api::HealthcheckGetError; + +pub trait ResponseContentErrorMapper { + fn map(self) -> String; +} + +impl ResponseContentErrorMapper for HealthcheckGetError { + fn map(self) -> String { + match self { + HealthcheckGetError::UnknownValue(value) => value.to_string(), + } + } +} + +impl ResponseContentErrorMapper for V1ApiDeploymentsGetError { + fn map(self) -> String { + match self { + V1ApiDeploymentsGetError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDeploymentsGetError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDeploymentsGetError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDeploymentsGetError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDeploymentsGetError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDeploymentsGetError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDeploymentsGetError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDeploymentsPutError { + fn map(self) -> String { + match self { + V1ApiDeploymentsPutError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDeploymentsPutError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDeploymentsPutError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDeploymentsPutError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDeploymentsPutError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDeploymentsPutError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDeploymentsPutError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDeploymentsDeleteError { + fn map(self) -> String { + match self { + V1ApiDeploymentsDeleteError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDeploymentsDeleteError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDeploymentsDeleteError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDeploymentsDeleteError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDeploymentsDeleteError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDeploymentsDeleteError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDeploymentsDeleteError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDefinitionsGetError { + fn map(self) -> String { + match self { + V1ApiDefinitionsGetError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDefinitionsGetError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDefinitionsGetError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDefinitionsGetError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDefinitionsGetError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDefinitionsGetError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDefinitionsGetError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDefinitionsPutError { + fn map(self) -> String { + match self { + V1ApiDefinitionsPutError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDefinitionsPutError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDefinitionsPutError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDefinitionsPutError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDefinitionsPutError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDefinitionsPutError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDefinitionsPutError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDefinitionsDeleteError { + fn map(self) -> String { + match self { + V1ApiDefinitionsDeleteError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDefinitionsDeleteError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDefinitionsDeleteError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDefinitionsDeleteError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDefinitionsDeleteError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDefinitionsDeleteError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDefinitionsDeleteError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiCertificatesGetError { + fn map(self) -> String { + match self { + V1ApiCertificatesGetError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiCertificatesGetError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiCertificatesGetError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiCertificatesGetError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiCertificatesGetError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiCertificatesGetError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiCertificatesGetError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiCertificatesPostError { + fn map(self) -> String { + match self { + V1ApiCertificatesPostError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiCertificatesPostError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiCertificatesPostError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiCertificatesPostError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiCertificatesPostError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiCertificatesPostError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiCertificatesPostError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiCertificatesDeleteError { + fn map(self) -> String { + match self { + V1ApiCertificatesDeleteError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiCertificatesDeleteError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiCertificatesDeleteError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiCertificatesDeleteError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiCertificatesDeleteError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiCertificatesDeleteError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiCertificatesDeleteError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDomainsGetError { + fn map(self) -> String { + match self { + V1ApiDomainsGetError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDomainsGetError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDomainsGetError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDomainsGetError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDomainsGetError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDomainsGetError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDomainsGetError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDomainsPutError { + fn map(self) -> String { + match self { + V1ApiDomainsPutError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDomainsPutError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDomainsPutError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDomainsPutError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDomainsPutError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDomainsPutError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDomainsPutError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} + +impl ResponseContentErrorMapper for V1ApiDomainsDeleteError { + fn map(self) -> String { + match self { + V1ApiDomainsDeleteError::Status400(errors) => { + // FIXME: fix schema interpretation + format!("BadRequest: {errors:?}") + } + V1ApiDomainsDeleteError::Status401(error) => { + format!("Unauthorized: {error:?}") + } + V1ApiDomainsDeleteError::Status403(error) => { + format!("LimitExceeded: {error:?}") + } + V1ApiDomainsDeleteError::Status404(message) => { + format!("NotFound: {message:?}") + } + V1ApiDomainsDeleteError::Status409(string) => { + format!("AlreadyExists: {string:?}") + } + V1ApiDomainsDeleteError::Status500(error) => { + format!("InternalError: {error:?}") + } + V1ApiDomainsDeleteError::UnknownValue(value) => { + format!("Unexpected error: {value:?}") + } + } + } +} \ No newline at end of file diff --git a/src/clients/gateway/healthcheck.rs b/src/clients/gateway/healthcheck.rs new file mode 100644 index 0000000..12d7794 --- /dev/null +++ b/src/clients/gateway/healthcheck.rs @@ -0,0 +1,27 @@ +use async_trait::async_trait; +use golem_gateway_client::apis::configuration::Configuration; +use golem_gateway_client::apis::healthcheck_api::healthcheck_get; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait HealthcheckClient { + async fn healthcheck(&self) -> Result<(), GolemError>; +} + +pub struct HealthcheckClientLive { + pub configuration: Configuration, +} + +#[async_trait] +impl HealthcheckClient for HealthcheckClientLive { + async fn healthcheck(&self) -> Result<(), GolemError> { + info!( + "Calling healthcheck_get on base url: {}", + self.configuration.base_path + ); + healthcheck_get(&self.configuration).await?; + Ok(()) + } +} diff --git a/src/clients/project.rs b/src/clients/project.rs index 5897b5a..bfa6fa8 100644 --- a/src/clients/project.rs +++ b/src/clients/project.rs @@ -31,6 +31,17 @@ pub trait ProjectClient { project_ref: ProjectRef, auth: &CloudAuthentication, ) -> Result, GolemError>; + + async fn resolve_id_or_default( + &self, + project_ref: ProjectRef, + auth: &CloudAuthentication, + ) -> Result { + match self.resolve_id(project_ref, auth).await? { + None => Ok(ProjectId(self.find_default(auth).await?.project_id)), + Some(project_id) => Ok(project_id), + } + } } pub struct ProjectClientLive { diff --git a/src/gateway.rs b/src/gateway.rs new file mode 100644 index 0000000..b543688 --- /dev/null +++ b/src/gateway.rs @@ -0,0 +1,145 @@ +mod deployment; +mod healthcheck; +mod definition; +mod certificate; +mod domain; + +use async_trait::async_trait; +use clap::Subcommand; +use golem_gateway_client::apis::configuration::Configuration; + +use crate::clients::gateway::deployment::DeploymentClientLive; +use crate::clients::gateway::healthcheck::HealthcheckClientLive; +use crate::clients::project::ProjectClient; +use crate::clients::CloudAuthentication; +use crate::clients::gateway::certificate::CertificateClientLive; +use crate::clients::gateway::definition::DefinitionClientLive; +use crate::clients::gateway::domain::DomainClientLive; +use crate::gateway::certificate::{CertificateHandler, CertificateHandlerLive, CertificateSubcommand}; +use crate::gateway::definition::{DefinitionHandler, DefinitionHandlerLive, DefinitionSubcommand}; +use crate::gateway::deployment::{DeploymentHandler, DeploymentHandlerLive, DeploymentSubcommand}; +use crate::gateway::domain::{DomainHandler, DomainHandlerLive, DomainSubcommand}; +use crate::gateway::healthcheck::{HealthcheckHandler, HealthcheckHandlerLive}; +use crate::model::{Format, GolemError, GolemResult}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum GatewaySubcommand { + #[command()] + Certificate { + #[command(subcommand)] + subcommand: CertificateSubcommand, + }, + #[command()] + Definition { + #[command(subcommand)] + subcommand: DefinitionSubcommand, + }, + #[command()] + Deployment { + #[command(subcommand)] + subcommand: DeploymentSubcommand, + }, + #[command()] + Domain { + #[command(subcommand)] + subcommand: DomainSubcommand, + }, + #[command()] + Healthcheck {}, +} + +#[async_trait] +pub trait GatewayHandler { + async fn handle( + &self, + format: Format, + token: &CloudAuthentication, + subcommand: GatewaySubcommand, + ) -> Result; +} + +pub struct GatewayHandlerLive<'p, P: ProjectClient + Sync + Send> { + pub base_url: reqwest::Url, + pub allow_insecure: bool, + pub projects: &'p P, +} + +#[async_trait] +impl<'p, P: ProjectClient + Sync + Send> GatewayHandler for GatewayHandlerLive<'p, P> { + async fn handle( + &self, + format: Format, + auth: &CloudAuthentication, + subcommand: GatewaySubcommand, + ) -> Result { + let mut builder = reqwest::Client::builder(); + if self.allow_insecure { + builder = builder.danger_accept_invalid_certs(true); + } + let client = builder.connection_verbose(true).build()?; + + let mut base_url_string = self.base_url.to_string(); + + if base_url_string.pop() != Some('/') { + base_url_string = self.base_url.to_string(); + } + + let configuration = Configuration { + base_path: base_url_string, + user_agent: None, + client: client, + basic_auth: None, + oauth_access_token: None, + bearer_access_token: Some(auth.0.secret.value.to_string()), + api_key: None, + }; + + let healthcheck_client = HealthcheckClientLive { + configuration: configuration.clone(), + }; + let healthcheck_srv = HealthcheckHandlerLive { + healthcheck: healthcheck_client, + }; + + let deployment_client = DeploymentClientLive { + configuration: configuration.clone(), + }; + let deployment_srv = DeploymentHandlerLive { + client: deployment_client, + projects: self.projects, + }; + + let definition_client = DefinitionClientLive { + configuration: configuration.clone(), + }; + let definition_srv = DefinitionHandlerLive { + client: definition_client, + projects: self.projects, + }; + + let certificate_client = CertificateClientLive { + configuration: configuration.clone(), + }; + let certificate_srv = CertificateHandlerLive { + client: certificate_client, + projects: self.projects, + }; + + let domain_client = DomainClientLive { + configuration: configuration.clone(), + }; + let domain_srv = DomainHandlerLive { + client: domain_client, + projects: self.projects, + }; + + match subcommand { + GatewaySubcommand::Certificate { subcommand } => certificate_srv.handle(auth, subcommand).await, + GatewaySubcommand::Definition { subcommand } => definition_srv.handle(format, auth, subcommand).await, + GatewaySubcommand::Deployment { subcommand } => deployment_srv.handle(auth, subcommand).await, + GatewaySubcommand::Domain { subcommand } => domain_srv.handle(auth, subcommand).await, + GatewaySubcommand::Healthcheck { } => healthcheck_srv.handle().await, + } + } +} diff --git a/src/gateway/certificate.rs b/src/gateway/certificate.rs new file mode 100644 index 0000000..0eab643 --- /dev/null +++ b/src/gateway/certificate.rs @@ -0,0 +1,128 @@ +use std::fs::File; +use std::io; +use std::io::{BufReader, Read}; +use async_trait::async_trait; +use clap::Subcommand; +use golem_gateway_client::models::CertificateRequest; +use crate::clients::CloudAuthentication; +use crate::clients::gateway::certificate::CertificateClient; +use crate::clients::project::ProjectClient; +use crate::model::{GolemError, GolemResult, PathBufOrStdin, ProjectRef}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum CertificateSubcommand { + #[command()] + Get { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(value_name = "certificate-id", value_hint = clap::ValueHint::Other)] + certificate_id: Option, + }, + #[command()] + Add { + #[command(flatten)] + project_ref: ProjectRef, + + #[arg(short, long, value_hint = clap::ValueHint::Other)] + domain_name: String, + + #[arg(short = 'b', long, value_name = "file", value_hint = clap::ValueHint::FilePath)] + certificate_body: PathBufOrStdin, + + #[arg(short = 'k', long, value_name = "file", value_hint = clap::ValueHint::FilePath)] + certificate_private_key: PathBufOrStdin, + }, + #[command()] + Delete { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(value_name = "certificate-id", value_hint = clap::ValueHint::Other)] + certificate_id: String, + }, +} + +#[async_trait] +pub trait CertificateHandler { + async fn handle( + &self, + auth: &CloudAuthentication, + command: CertificateSubcommand, + ) -> Result; +} + +pub struct CertificateHandlerLive< + 'p, + C: CertificateClient + Sync + Send, + P: ProjectClient + Sync + Send, +> { + pub client: C, + pub projects: &'p P, +} + +fn read_as_string( + mut r: R, + source: &str, +) -> Result { + let mut result = String::new(); + + r.read_to_string(&mut result).map_err(|e| { + GolemError(format!( + "Failed to read {source} as String: ${e}" + )) + })?; + + Ok(result) +} + +fn read_path_or_stdin_as_string(path_or_stdin: PathBufOrStdin) -> Result { + match path_or_stdin { + PathBufOrStdin::Path(path) => { + let file = File::open(&path).map_err(|e| { + GolemError(format!("Failed to open file {path:?}: {e}")) + })?; + + let reader = BufReader::new(file); + + read_as_string(reader, &format!("file `{path:?}`")) + } + PathBufOrStdin::Stdin => read_as_string(io::stdin(), "stdin"), + } +} + +#[async_trait] +impl<'p, C: CertificateClient + Sync + Send, P: ProjectClient + Sync + Send> CertificateHandler for CertificateHandlerLive<'p, C, P>{ + async fn handle(&self, auth: &CloudAuthentication, command: CertificateSubcommand) -> Result { + match command { + CertificateSubcommand::Get { project_ref, certificate_id } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + + let res = self.client.get(project_id, certificate_id.as_deref()).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + CertificateSubcommand::Add { project_ref, domain_name, certificate_body, certificate_private_key } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + + let request = CertificateRequest { + project_id: project_id.0, + domain_name, + certificate_body: read_path_or_stdin_as_string(certificate_body)?, + certificate_private_key: read_path_or_stdin_as_string(certificate_private_key)? + }; + + let res = self.client.create(request).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + CertificateSubcommand::Delete { project_ref, certificate_id } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + let res = self + .client + .delete(project_id, &certificate_id) + .await?; + Ok(GolemResult::Ok(Box::new(res))) + } + } + } +} diff --git a/src/gateway/definition.rs b/src/gateway/definition.rs new file mode 100644 index 0000000..a6d5274 --- /dev/null +++ b/src/gateway/definition.rs @@ -0,0 +1,115 @@ +use std::fs::File; +use std::io; +use std::io::{BufReader, Read}; +use async_trait::async_trait; +use clap::Subcommand; +use golem_gateway_client::models::ApiDefinition; +use crate::clients::CloudAuthentication; +use crate::clients::gateway::definition::DefinitionClient; +use crate::clients::project::ProjectClient; +use crate::model::{Format, GolemError, GolemResult, PathBufOrStdin, ProjectRef}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum DefinitionSubcommand { + #[command()] + Get { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(value_name = "api-definition-id", value_hint = clap::ValueHint::Other)] + definition_id: Option, + }, + #[command()] + Update { + #[arg(value_name = "definition-file", value_hint = clap::ValueHint::FilePath)] + definition_file: Option, + }, + #[command()] + Delete { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(value_name = "api-definition-id", value_hint = clap::ValueHint::Other)] + definition_id: String, + }, +} + +#[async_trait] +pub trait DefinitionHandler { + async fn handle( + &self, + format: Format, + auth: &CloudAuthentication, + command: DefinitionSubcommand, + ) -> Result; +} + +pub struct DefinitionHandlerLive< + 'p, + C: DefinitionClient + Sync + Send, + P: ProjectClient + Sync + Send, +> { + pub client: C, + pub projects: &'p P, +} + +fn read_definition( + format: Format, + r: R, + source: &str, +) -> Result { + let api_definition: ApiDefinition = match format { + Format::Json => serde_json::from_reader(r).map_err(|e| { + GolemError(format!( + "Failed to parse ApiDefinition from {source} as json: ${e}" + )) + })?, + Format::Yaml => serde_yaml::from_reader(r).map_err(|e| { + GolemError(format!( + "Failed to parse ApiDefinition from {source} as yaml: ${e}" + )) + })?, + }; + + Ok(api_definition) +} + +#[async_trait] +impl<'p, C: DefinitionClient + Sync + Send, P: ProjectClient + Sync + Send> DefinitionHandler for DefinitionHandlerLive<'p, C, P>{ + async fn handle(&self, format: Format, auth: &CloudAuthentication, command: DefinitionSubcommand) -> Result { + match command { + DefinitionSubcommand::Get { project_ref, definition_id } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + + let res = self.client.get(project_id, definition_id.as_deref()).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DefinitionSubcommand::Update { definition_file } => { + let definition = match definition_file.unwrap_or(PathBufOrStdin::Stdin) { + PathBufOrStdin::Path(path) => { + let file = File::open(&path).map_err(|e| { + GolemError(format!("Failed to open file {path:?}: {e}")) + })?; + + let reader = BufReader::new(file); + + read_definition(format, reader, &format!("file `{path:?}`"))? + } + PathBufOrStdin::Stdin => read_definition(format, io::stdin(), "stdin")?, + }; + + let res = self.client.update(definition).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DefinitionSubcommand::Delete { project_ref, definition_id } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + let res = self + .client + .delete(project_id, &definition_id) + .await?; + Ok(GolemResult::Ok(Box::new(res))) + } + } + } +} \ No newline at end of file diff --git a/src/gateway/deployment.rs b/src/gateway/deployment.rs new file mode 100644 index 0000000..7cf5357 --- /dev/null +++ b/src/gateway/deployment.rs @@ -0,0 +1,110 @@ +use async_trait::async_trait; +use clap::Subcommand; +use golem_gateway_client::models::{ApiDeployment, ApiSite}; + +use crate::clients::gateway::deployment::DeploymentClient; +use crate::clients::project::ProjectClient; +use crate::clients::CloudAuthentication; +use crate::model::{GolemError, GolemResult, ProjectRef}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum DeploymentSubcommand { + #[command()] + Get { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(short, long, value_name = "api-definition-id", value_hint = clap::ValueHint::Other)] + definition_id: String, + }, + #[command()] + Add { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(short, long, value_name = "api-definition-id", value_hint = clap::ValueHint::Other)] + definition_id: String, + #[arg(short = 'H', long, value_name = "site-host", value_hint = clap::ValueHint::Other)] + host: String, + #[arg(short, long, value_name = "site-subdomain", value_hint = clap::ValueHint::Other)] + subdomain: String, + }, + #[command()] + Delete { + #[command(flatten)] + project_ref: ProjectRef, + #[arg(short, long)] + site: String, + #[arg(short, long, value_name = "api-definition-id", value_hint = clap::ValueHint::Other)] + definition_id: String, + }, +} + +#[async_trait] +pub trait DeploymentHandler { + async fn handle( + &self, + auth: &CloudAuthentication, + command: DeploymentSubcommand, + ) -> Result; +} + +pub struct DeploymentHandlerLive< + 'p, + C: DeploymentClient + Sync + Send, + P: ProjectClient + Sync + Send, +> { + pub client: C, + pub projects: &'p P, +} + +#[async_trait] +impl<'p, C: DeploymentClient + Sync + Send, P: ProjectClient + Sync + Send> DeploymentHandler + for DeploymentHandlerLive<'p, C, P> +{ + async fn handle( + &self, + auth: &CloudAuthentication, + command: DeploymentSubcommand, + ) -> Result { + match command { + DeploymentSubcommand::Get { + project_ref, + definition_id, + } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + let res = self.client.get(project_id, &definition_id).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DeploymentSubcommand::Add { project_ref, definition_id, host, subdomain } => { + let deployment = ApiDeployment { + project_id: self.projects.resolve_id_or_default(project_ref, auth).await?.0, + api_definition_id: definition_id, + site: Box::new( + ApiSite { + host, + subdomain + } + + ) + }; + + let res = self.client.update(deployment).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DeploymentSubcommand::Delete { + project_ref, + site, + definition_id, + } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + let res = self + .client + .delete(project_id, &definition_id, &site) + .await?; + Ok(GolemResult::Ok(Box::new(res))) + } + } + } +} diff --git a/src/gateway/domain.rs b/src/gateway/domain.rs new file mode 100644 index 0000000..514a3a0 --- /dev/null +++ b/src/gateway/domain.rs @@ -0,0 +1,80 @@ +use async_trait::async_trait; +use clap::Subcommand; +use crate::clients::CloudAuthentication; +use crate::clients::gateway::domain::DomainClient; +use crate::clients::project::ProjectClient; +use crate::model::{GolemError, GolemResult, ProjectRef}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum DomainSubcommand { + #[command()] + Get { + #[command(flatten)] + project_ref: ProjectRef, + }, + #[command()] + Add { + #[command(flatten)] + project_ref: ProjectRef, + + #[arg(short, long, value_hint = clap::ValueHint::Other)] + domain_name: String, + }, + #[command()] + Delete { + #[command(flatten)] + project_ref: ProjectRef, + + #[arg(value_name = "domain-name", value_hint = clap::ValueHint::Other)] + domain_name: String, + }, +} + +#[async_trait] +pub trait DomainHandler { + async fn handle( + &self, + auth: &CloudAuthentication, + command: DomainSubcommand, + ) -> Result; +} + +pub struct DomainHandlerLive< + 'p, + C: DomainClient + Sync + Send, + P: ProjectClient + Sync + Send, +> { + pub client: C, + pub projects: &'p P, +} + +#[async_trait] +impl<'p, C: DomainClient + Sync + Send, P: ProjectClient + Sync + Send> DomainHandler for DomainHandlerLive<'p, C, P>{ + async fn handle(&self, auth: &CloudAuthentication, command: DomainSubcommand) -> Result { + match command { + DomainSubcommand::Get { project_ref } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + + let res = self.client.get(project_id).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DomainSubcommand::Add { project_ref, domain_name } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + + let res = self.client.update(project_id, domain_name).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + DomainSubcommand::Delete { project_ref, domain_name } => { + let project_id = self.projects.resolve_id_or_default(project_ref, auth).await?; + let res = self + .client + .delete(project_id, &domain_name) + .await?; + Ok(GolemResult::Ok(Box::new(res))) + } + } + } +} diff --git a/src/gateway/healthcheck.rs b/src/gateway/healthcheck.rs new file mode 100644 index 0000000..9a7bd37 --- /dev/null +++ b/src/gateway/healthcheck.rs @@ -0,0 +1,22 @@ +use async_trait::async_trait; + +use crate::clients::gateway::healthcheck::HealthcheckClient; +use crate::model::{GolemError, GolemResult}; + +#[async_trait] +pub trait HealthcheckHandler { + async fn handle(&self) -> Result; +} + +pub struct HealthcheckHandlerLive { + pub healthcheck: H, +} + +#[async_trait] +impl HealthcheckHandler for HealthcheckHandlerLive { + async fn handle(&self) -> Result { + self.healthcheck.healthcheck().await?; + + Ok(GolemResult::Str("Online".to_string())) + } +} diff --git a/src/main.rs b/src/main.rs index ca610d9..53626a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ use crate::clients::project_grant::ProjectGrantClientLive; use crate::clients::template::TemplateClientLive; use crate::clients::token::TokenClientLive; use crate::clients::worker::WorkerClientLive; +use crate::gateway::{GatewayHandler, GatewayHandlerLive, GatewaySubcommand}; use crate::policy::{ProjectPolicyHandler, ProjectPolicyHandlerLive, ProjectPolicySubcommand}; use crate::project::{ProjectHandler, ProjectHandlerLive, ProjectSubcommand}; use crate::project_grant::{ProjectGrantHandler, ProjectGrantHandlerLive}; @@ -40,6 +41,7 @@ mod account; mod auth; pub mod clients; mod examples; +mod gateway; pub mod model; mod policy; mod project; @@ -134,6 +136,11 @@ enum Command { #[arg(short, long)] language: Option, }, + #[command()] + Gateway { + #[command(subcommand)] + subcommand: GatewaySubcommand, + }, } #[derive(Parser, Debug)] @@ -186,7 +193,9 @@ fn main() -> Result<(), Box> { async fn async_main(cmd: GolemCommand) -> Result<(), Box> { let url_str = std::env::var("GOLEM_BASE_URL").unwrap_or("https://release.api.golem.cloud/".to_string()); + let gateway_url_str = std::env::var("GOLEM_GATEWAY_BASE_URL").unwrap_or(url_str.clone()); let url = Url::parse(&url_str).unwrap(); + let gateway_url = Url::parse(&gateway_url_str).unwrap(); let home = dirs::home_dir().unwrap(); let allow_insecure_str = std::env::var("GOLEM_ALLOW_INSECURE").unwrap_or("false".to_string()); let allow_insecure = allow_insecure_str != "false"; @@ -279,6 +288,11 @@ async fn async_main(cmd: GolemCommand) -> Result<(), Box> client: worker_client, templates: &template_srv, }; + let gateway_srv = GatewayHandlerLive { + base_url: gateway_url.clone(), + allow_insecure, + projects: &project_client, + }; let auth = auth_srv .authenticate( @@ -324,6 +338,7 @@ async fn async_main(cmd: GolemCommand) -> Result<(), Box> Command::ListExamples { min_tier, language } => { examples::process_list_examples(min_tier, language) } + Command::Gateway { subcommand } => gateway_srv.handle(cmd.format, &auth, subcommand).await, }; match res { diff --git a/src/model.rs b/src/model.rs index 8bacae6..be64489 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,5 +1,6 @@ use std::ffi::OsStr; use std::fmt::{Debug, Display, Formatter}; +use std::path::PathBuf; use std::str::FromStr; use clap::builder::{StringValueParser, TypedValueParser}; @@ -16,11 +17,14 @@ use golem_client::template::TemplateError; use golem_client::token::TokenError; use golem_client::worker::WorkerError; use golem_examples::model::{Example, ExampleName, GuestLanguage, GuestLanguageTier}; +use golem_gateway_client::apis::ResponseContent; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use uuid::Uuid; +use crate::clients::gateway::errors::ResponseContentErrorMapper; + pub enum GolemResult { Ok(Box), Json(serde_json::value::Value), @@ -75,6 +79,39 @@ impl From for GolemError { } } +impl From for GolemError { + fn from(error: reqwest::Error) -> Self { + GolemError(format!("Unexpected reqwest error: {error}")) + } +} + +impl From> for GolemError { + fn from(value: golem_gateway_client::apis::Error) -> Self { + match value { + golem_gateway_client::apis::Error::Reqwest(error) => GolemError::from(error), + golem_gateway_client::apis::Error::Serde(error) => { + GolemError(format!("Unexpected serde error: {error}")) + } + golem_gateway_client::apis::Error::Io(error) => { + GolemError(format!("Unexpected io error: {error}")) + } + golem_gateway_client::apis::Error::ResponseError(ResponseContent { + status, + content, + entity, + }) => match entity { + None => GolemError(format!( + "Response error. Status: {status}, content: {content}" + )), + Some(e) => { + let entity_str = ResponseContentErrorMapper::map(e); + GolemError(format!("Response error. Status: {status}, content: {content}, entity: {entity_str}")) + } + }, + } + } +} + impl From for GolemError { fn from(value: TokenError) -> Self { match value { @@ -711,3 +748,21 @@ impl ExampleDescription { } } } + +#[derive(Clone, Debug)] +pub enum PathBufOrStdin { + Path(PathBuf), + Stdin, +} + +impl FromStr for PathBufOrStdin { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + if s == "-" { + Ok(PathBufOrStdin::Stdin) + } else { + Ok(PathBufOrStdin::Path(PathBuf::from_str(s)?)) + } + } +}