From 8d1e834a55c2b883b23ea3def02164cfab3ea40e Mon Sep 17 00:00:00 2001 From: Simon Popugaev Date: Thu, 30 May 2024 15:16:08 +0300 Subject: [PATCH] 232 unify oss and cloud cli --- Cargo.lock | 40 + golem-cli/Cargo.toml | 14 + golem-cli/src/api_definition.rs | 292 -------- golem-cli/src/clients.rs | 1 - golem-cli/src/clients/api_definition.rs | 155 +--- golem-cli/src/clients/api_deployment.rs | 67 +- golem-cli/src/clients/component.rs | 117 +-- golem-cli/src/clients/health_check.rs | 20 +- golem-cli/src/clients/worker.rs | 433 +---------- golem-cli/src/cloud.rs | 20 + golem-cli/src/cloud/auth.rs | 196 +++++ golem-cli/src/cloud/clients.rs | 80 ++ golem-cli/src/cloud/clients/account.rs | 64 ++ golem-cli/src/cloud/clients/component.rs | 140 ++++ golem-cli/src/cloud/clients/errors.rs | 214 ++++++ golem-cli/src/cloud/clients/gateway.rs | 20 + .../cloud/clients/gateway/api_definition.rs | 232 ++++++ .../cloud/clients/gateway/api_deployment.rs | 92 +++ .../src/cloud/clients/gateway/certificate.rs | 68 ++ golem-cli/src/cloud/clients/gateway/domain.rs | 63 ++ golem-cli/src/cloud/clients/gateway/errors.rs | 124 +++ .../src/cloud/clients/gateway/health_check.rs | 41 + golem-cli/src/cloud/clients/grant.rs | 91 +++ golem-cli/src/cloud/clients/health_check.rs | 40 + golem-cli/src/cloud/clients/login.rs | 61 ++ golem-cli/src/cloud/clients/policy.rs | 63 ++ golem-cli/src/cloud/clients/project.rs | 78 ++ golem-cli/src/cloud/clients/project_grant.rs | 82 ++ golem-cli/src/cloud/clients/token.rs | 71 ++ golem-cli/src/cloud/clients/worker.rs | 572 ++++++++++++++ golem-cli/src/cloud/command.rs | 200 +++++ golem-cli/src/cloud/command/account.rs | 117 +++ golem-cli/src/cloud/command/api_definition.rs | 163 ++++ golem-cli/src/cloud/command/api_deployment.rs | 102 +++ golem-cli/src/cloud/command/certificate.rs | 86 +++ golem-cli/src/cloud/command/component.rs | 120 +++ golem-cli/src/cloud/command/domain.rs | 64 ++ golem-cli/src/cloud/command/policy.rs | 59 ++ golem-cli/src/cloud/command/project.rs | 62 ++ golem-cli/src/cloud/command/token.rs | 66 ++ golem-cli/src/cloud/command/worker.rs | 396 ++++++++++ golem-cli/src/cloud/factory.rs | 366 +++++++++ golem-cli/src/cloud/model.rs | 343 +++++++++ golem-cli/src/cloud/model/text.rs | 300 ++++++++ golem-cli/src/cloud/service.rs | 22 + golem-cli/src/cloud/service/account.rs | 87 +++ golem-cli/src/cloud/service/certificate.rs | 119 +++ golem-cli/src/cloud/service/domain.rs | 72 ++ golem-cli/src/cloud/service/grant.rs | 70 ++ golem-cli/src/cloud/service/policy.rs | 54 ++ golem-cli/src/cloud/service/project.rs | 119 +++ golem-cli/src/cloud/service/project_grant.rs | 68 ++ golem-cli/src/cloud/service/token.rs | 68 ++ golem-cli/src/cloud_main.rs | 204 +++++ golem-cli/src/component.rs | 200 ----- golem-cli/src/factory.rs | 154 ++++ golem-cli/src/lib.rs | 10 +- golem-cli/src/main.rs | 274 +------ golem-cli/src/model.rs | 323 +++++++- golem-cli/src/model/component.rs | 299 +++++++- golem-cli/src/model/invoke_result_view.rs | 8 +- golem-cli/src/model/text.rs | 9 +- golem-cli/src/oss.rs | 18 + golem-cli/src/oss/clients.rs | 20 + golem-cli/src/oss/clients/api_definition.rs | 170 +++++ golem-cli/src/oss/clients/api_deployment.rs | 86 +++ golem-cli/src/oss/clients/component.rs | 132 ++++ golem-cli/src/{ => oss}/clients/errors.rs | 7 +- golem-cli/src/oss/clients/health_check.rs | 36 + golem-cli/src/oss/clients/worker.rs | 441 +++++++++++ golem-cli/src/oss/command.rs | 118 +++ golem-cli/src/oss/command/api_definition.rs | 106 +++ .../src/{ => oss/command}/api_deployment.rs | 47 +- golem-cli/src/oss/command/component.rs | 94 +++ golem-cli/src/oss/command/worker.rs | 378 ++++++++++ golem-cli/src/oss/factory.rs | 148 ++++ golem-cli/src/oss/model.rs | 28 + golem-cli/src/service.rs | 19 + golem-cli/src/service/api_definition.rs | 130 ++++ golem-cli/src/service/api_deployment.rs | 87 +++ golem-cli/src/service/component.rs | 203 +++++ golem-cli/src/service/version.rs | 62 ++ golem-cli/src/service/worker.rs | 596 +++++++++++++++ golem-cli/src/stubgen.rs | 39 + golem-cli/src/version.rs | 159 ---- golem-cli/src/worker.rs | 708 ------------------ 86 files changed, 9584 insertions(+), 2403 deletions(-) delete mode 100644 golem-cli/src/api_definition.rs create mode 100644 golem-cli/src/cloud.rs create mode 100644 golem-cli/src/cloud/auth.rs create mode 100644 golem-cli/src/cloud/clients.rs create mode 100644 golem-cli/src/cloud/clients/account.rs create mode 100644 golem-cli/src/cloud/clients/component.rs create mode 100644 golem-cli/src/cloud/clients/errors.rs create mode 100644 golem-cli/src/cloud/clients/gateway.rs create mode 100644 golem-cli/src/cloud/clients/gateway/api_definition.rs create mode 100644 golem-cli/src/cloud/clients/gateway/api_deployment.rs create mode 100644 golem-cli/src/cloud/clients/gateway/certificate.rs create mode 100644 golem-cli/src/cloud/clients/gateway/domain.rs create mode 100644 golem-cli/src/cloud/clients/gateway/errors.rs create mode 100644 golem-cli/src/cloud/clients/gateway/health_check.rs create mode 100644 golem-cli/src/cloud/clients/grant.rs create mode 100644 golem-cli/src/cloud/clients/health_check.rs create mode 100644 golem-cli/src/cloud/clients/login.rs create mode 100644 golem-cli/src/cloud/clients/policy.rs create mode 100644 golem-cli/src/cloud/clients/project.rs create mode 100644 golem-cli/src/cloud/clients/project_grant.rs create mode 100644 golem-cli/src/cloud/clients/token.rs create mode 100644 golem-cli/src/cloud/clients/worker.rs create mode 100644 golem-cli/src/cloud/command.rs create mode 100644 golem-cli/src/cloud/command/account.rs create mode 100644 golem-cli/src/cloud/command/api_definition.rs create mode 100644 golem-cli/src/cloud/command/api_deployment.rs create mode 100644 golem-cli/src/cloud/command/certificate.rs create mode 100644 golem-cli/src/cloud/command/component.rs create mode 100644 golem-cli/src/cloud/command/domain.rs create mode 100644 golem-cli/src/cloud/command/policy.rs create mode 100644 golem-cli/src/cloud/command/project.rs create mode 100644 golem-cli/src/cloud/command/token.rs create mode 100644 golem-cli/src/cloud/command/worker.rs create mode 100644 golem-cli/src/cloud/factory.rs create mode 100644 golem-cli/src/cloud/model.rs create mode 100644 golem-cli/src/cloud/model/text.rs create mode 100644 golem-cli/src/cloud/service.rs create mode 100644 golem-cli/src/cloud/service/account.rs create mode 100644 golem-cli/src/cloud/service/certificate.rs create mode 100644 golem-cli/src/cloud/service/domain.rs create mode 100644 golem-cli/src/cloud/service/grant.rs create mode 100644 golem-cli/src/cloud/service/policy.rs create mode 100644 golem-cli/src/cloud/service/project.rs create mode 100644 golem-cli/src/cloud/service/project_grant.rs create mode 100644 golem-cli/src/cloud/service/token.rs create mode 100644 golem-cli/src/cloud_main.rs delete mode 100644 golem-cli/src/component.rs create mode 100644 golem-cli/src/factory.rs create mode 100644 golem-cli/src/oss.rs create mode 100644 golem-cli/src/oss/clients.rs create mode 100644 golem-cli/src/oss/clients/api_definition.rs create mode 100644 golem-cli/src/oss/clients/api_deployment.rs create mode 100644 golem-cli/src/oss/clients/component.rs rename golem-cli/src/{ => oss}/clients/errors.rs (98%) create mode 100644 golem-cli/src/oss/clients/health_check.rs create mode 100644 golem-cli/src/oss/clients/worker.rs create mode 100644 golem-cli/src/oss/command.rs create mode 100644 golem-cli/src/oss/command/api_definition.rs rename golem-cli/src/{ => oss/command}/api_deployment.rs (57%) create mode 100644 golem-cli/src/oss/command/component.rs create mode 100644 golem-cli/src/oss/command/worker.rs create mode 100644 golem-cli/src/oss/factory.rs create mode 100644 golem-cli/src/oss/model.rs create mode 100644 golem-cli/src/service.rs create mode 100644 golem-cli/src/service/api_definition.rs create mode 100644 golem-cli/src/service/api_deployment.rs create mode 100644 golem-cli/src/service/component.rs create mode 100644 golem-cli/src/service/version.rs create mode 100644 golem-cli/src/service/worker.rs create mode 100644 golem-cli/src/stubgen.rs delete mode 100644 golem-cli/src/version.rs delete mode 100644 golem-cli/src/worker.rs diff --git a/Cargo.lock b/Cargo.lock index 8b6a4389c..df64986d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3030,17 +3030,21 @@ dependencies = [ name = "golem-cli" version = "0.0.0" dependencies = [ + "anyhow", "async-recursion", "async-trait", "chrono", "clap 4.5.4", "clap-verbosity-flag", "cli-table", + "colored", "derive_more", "dirs 5.0.1", "env_logger 0.11.3", "futures-util", "golem-client", + "golem-cloud-client", + "golem-cloud-worker-client", "golem-examples", "golem-test-framework", "golem-wasm-ast", @@ -3103,6 +3107,42 @@ dependencies = [ "uuid", ] +[[package]] +name = "golem-cloud-client" +version = "0.0.0" +dependencies = [ + "async-trait", + "bytes 1.6.0", + "chrono", + "futures-core", + "golem-openapi-client-generator", + "http 1.1.0", + "relative-path", + "reqwest", + "serde", + "serde_json", + "tracing", + "uuid", +] + +[[package]] +name = "golem-cloud-worker-client" +version = "0.0.0" +dependencies = [ + "async-trait", + "bytes 1.6.0", + "chrono", + "futures-core", + "golem-openapi-client-generator", + "http 1.1.0", + "relative-path", + "reqwest", + "serde", + "serde_json", + "tracing", + "uuid", +] + [[package]] name = "golem-common" version = "0.0.0" diff --git a/golem-cli/Cargo.toml b/golem-cli/Cargo.toml index 5192aad68..846250c71 100644 --- a/golem-cli/Cargo.toml +++ b/golem-cli/Cargo.toml @@ -10,6 +10,14 @@ readme = "README.md" description = "Command line interface for OSS version of Golem. See also golem-cloud-cli." autotests = false +[[bin]] +name = "golem-cli" +path = "src/main.rs" + +[[bin]] +name = "golem-cloud-cli" +path = "src/cloud_main.rs" + [[test]] name = "integration" path = "tests/main.rs" @@ -18,6 +26,10 @@ harness = false [dependencies] golem-client = { path = "../golem-client", version = "0.0.0" } +golem-cloud-client = { path = "../../golem-cloud/golem-cloud-client", version="0.0.0" } +golem-cloud-worker-client = { path = "../../golem-cloud/golem-cloud-worker-client", version="0.0.0" } + + async-trait = { workspace = true } chrono = { workspace = true } clap = { workspace = true } @@ -56,6 +68,8 @@ version-compare = "=0.0.11" wasm-wave = { workspace = true } cli-table = { workspace = true } textwrap = "0.16.1" +anyhow.workspace = true +colored = "2.1.0" [dev-dependencies] golem-test-framework = { path = "../golem-test-framework", version = "0.0.0" } diff --git a/golem-cli/src/api_definition.rs b/golem-cli/src/api_definition.rs deleted file mode 100644 index 02dcc998b..000000000 --- a/golem-cli/src/api_definition.rs +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2024 Golem Cloud -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use async_trait::async_trait; - -use crate::clients::api_definition::ApiDefinitionClient; -use crate::model::text::{ - ApiDefinitionAddRes, ApiDefinitionGetRes, ApiDefinitionImportRes, ApiDefinitionUpdateRes, -}; -use crate::model::{ - ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult, PathBufOrStdin, -}; -use clap::Subcommand; - -#[derive(Subcommand, Debug)] -#[command()] -pub enum ApiDefinitionSubcommand { - /// Lists all api definitions - #[command()] - List { - /// Api definition id to get all versions. Optional. - #[arg(short, long)] - id: Option, - }, - - /// Creates an api definition - /// - /// Golem API definition file format expected - #[command()] - Add { - /// The Golem API definition file - #[arg(value_hint = clap::ValueHint::FilePath)] - definition: PathBufOrStdin, // TODO: validate exists - }, - - /// Updates an api definition - /// - /// Golem API definition file format expected - #[command()] - Update { - /// The Golem API definition file - #[arg(value_hint = clap::ValueHint::FilePath)] - definition: PathBufOrStdin, // TODO: validate exists - }, - - /// Import OpenAPI file as api definition - #[command()] - Import { - /// The OpenAPI json or yaml file to be used as the api definition - /// - /// Json format expected unless file name ends up in `.yaml` - #[arg(value_hint = clap::ValueHint::FilePath)] - definition: PathBufOrStdin, // TODO: validate exists - }, - - /// Retrieves metadata about an existing api definition - #[command()] - Get { - /// Api definition id - #[arg(short, long)] - id: ApiDefinitionId, - - /// Version of the api definition - #[arg(short = 'V', long)] - version: ApiDefinitionVersion, - }, - - /// Deletes an existing api definition - #[command()] - Delete { - /// Api definition id - #[arg(short, long)] - id: ApiDefinitionId, - - /// Version of the api definition - #[arg(short = 'V', long)] - version: ApiDefinitionVersion, - }, -} - -#[async_trait] -pub trait ApiDefinitionHandler { - async fn handle(&self, subcommand: ApiDefinitionSubcommand) -> Result; -} - -pub struct ApiDefinitionHandlerLive { - pub client: C, -} - -#[async_trait] -impl ApiDefinitionHandler for ApiDefinitionHandlerLive { - async fn handle(&self, subcommand: ApiDefinitionSubcommand) -> Result { - match subcommand { - ApiDefinitionSubcommand::Get { id, version } => { - let definition = self.client.get(id, version).await?; - Ok(GolemResult::Ok(Box::new(ApiDefinitionGetRes(definition)))) - } - ApiDefinitionSubcommand::Add { definition } => { - let definition = self.client.create(definition).await?; - Ok(GolemResult::Ok(Box::new(ApiDefinitionAddRes(definition)))) - } - ApiDefinitionSubcommand::Update { definition } => { - let definition = self.client.update(definition).await?; - Ok(GolemResult::Ok(Box::new(ApiDefinitionUpdateRes( - definition, - )))) - } - ApiDefinitionSubcommand::Import { definition } => { - let definition = self.client.import(definition).await?; - Ok(GolemResult::Ok(Box::new(ApiDefinitionImportRes( - definition, - )))) - } - ApiDefinitionSubcommand::List { id } => { - let definitions = self.client.list(id.as_ref()).await?; - Ok(GolemResult::Ok(Box::new(definitions))) - } - ApiDefinitionSubcommand::Delete { id, version } => { - let result = self.client.delete(id, version).await?; - Ok(GolemResult::Str(result)) - } - } - } -} - -#[cfg(test)] -mod tests { - - use std::sync::{Arc, Mutex}; - - use golem_client::{api::ApiDefinitionError, model::HttpApiDefinition}; - use tonic::async_trait; - - use crate::{ - api_definition::ApiDefinitionSubcommand, - clients::api_definition::ApiDefinitionClientLive, - model::{ApiDefinitionId, ApiDefinitionVersion, GolemError}, - }; - use golem_client::Error; - - use super::{ApiDefinitionHandler, ApiDefinitionHandlerLive}; - - pub struct ApiDefinitionClientTest { - calls: Arc>, - } - - #[async_trait] - impl golem_client::api::ApiDefinitionClient for ApiDefinitionClientTest { - async fn import_open_api( - &self, - _: &serde_json::Value, - ) -> Result> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str("oas_put"); - - Ok(HttpApiDefinition { - id: "".to_string(), - version: "".to_string(), - routes: vec![], - draft: false, - }) - } - - async fn get_definition( - &self, - api_definition_id: &str, - version: &str, - ) -> Result> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str(format!("get: {}/{}", api_definition_id, version).as_str()); - Ok(HttpApiDefinition { - id: "".to_string(), - version: "".to_string(), - routes: vec![], - draft: false, - }) - } - - async fn update_definition( - &self, - _id: &str, - _version: &str, - value: &HttpApiDefinition, - ) -> Result> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str(format!("put: {:?}", value).as_str()); - Ok(HttpApiDefinition { - id: "".to_string(), - version: "".to_string(), - routes: vec![], - draft: false, - }) - } - - async fn create_definition( - &self, - value: &HttpApiDefinition, - ) -> Result> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str(format!("post: {:?}", value).as_str()); - Ok(HttpApiDefinition { - id: "".to_string(), - version: "".to_string(), - routes: vec![], - draft: false, - }) - } - - async fn delete_definition( - &self, - api_definition_id: &str, - version: &str, - ) -> Result> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str(format!("delete: {}/{}", api_definition_id, version).as_str()); - Ok("deleted".to_string()) - } - - async fn list_definitions( - &self, - _id: Option<&str>, - ) -> Result, Error> { - let mut calls = self.calls.lock().unwrap(); - calls.push_str("all"); - Ok(vec![]) - } - } - - async fn handle(subcommand: ApiDefinitionSubcommand) -> Result { - let api_definition_client = ApiDefinitionClientLive { - client: ApiDefinitionClientTest { - calls: Arc::new(Mutex::new(String::new())), - }, - }; - - let api_definition_srv = ApiDefinitionHandlerLive { - client: api_definition_client, - }; - api_definition_srv.handle(subcommand).await.map(|_| { - api_definition_srv - .client - .client - .calls - .lock() - .unwrap() - .to_string() - }) - } - - #[tokio::test] - pub async fn list() { - let checked = handle(ApiDefinitionSubcommand::List { id: None }).await; - if let Ok(calls) = checked { - assert_eq!(calls, "all"); - } - } - - #[tokio::test] - pub async fn get() { - let subcommand = ApiDefinitionSubcommand::Get { - id: ApiDefinitionId("id".to_string()), - version: ApiDefinitionVersion("version".to_string()), - }; - let checked = handle(subcommand).await; - if let Ok(calls) = checked { - assert_eq!(calls, "get: id/version"); - } - } - - #[tokio::test] - pub async fn delete() { - let subcommand = ApiDefinitionSubcommand::Delete { - id: ApiDefinitionId("id".to_string()), - version: ApiDefinitionVersion("version".to_string()), - }; - let checked = handle(subcommand).await; - if let Ok(calls) = checked { - assert_eq!(calls, "delete: id/version"); - } - } -} diff --git a/golem-cli/src/clients.rs b/golem-cli/src/clients.rs index b874f553e..59a8c5623 100644 --- a/golem-cli/src/clients.rs +++ b/golem-cli/src/clients.rs @@ -15,6 +15,5 @@ pub mod api_definition; pub mod api_deployment; pub mod component; -pub mod errors; pub mod health_check; pub mod worker; diff --git a/golem-cli/src/clients/api_definition.rs b/golem-cli/src/clients/api_definition.rs index 9e1e43853..b64b71142 100644 --- a/golem-cli/src/clients/api_definition.rs +++ b/golem-cli/src/clients/api_definition.rs @@ -12,161 +12,44 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Display; - -use std::io::Read; - +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, PathBufOrStdin}; use async_trait::async_trait; - use golem_client::model::HttpApiDefinition; -use tokio::fs::read_to_string; -use tracing::info; - -use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, PathBufOrStdin}; - #[async_trait] pub trait ApiDefinitionClient { + type ProjectContext; + async fn list( &self, id: Option<&ApiDefinitionId>, + project: &Self::ProjectContext, ) -> Result, GolemError>; async fn get( &self, id: ApiDefinitionId, version: ApiDefinitionVersion, + project: &Self::ProjectContext, ) -> Result; - async fn create(&self, path: PathBufOrStdin) -> Result; - async fn update(&self, path: PathBufOrStdin) -> Result; - async fn import(&self, path: PathBufOrStdin) -> Result; - async fn delete( + async fn create( &self, - id: ApiDefinitionId, - version: ApiDefinitionVersion, - ) -> Result; -} - -#[derive(Clone)] -pub struct ApiDefinitionClientLive { - pub client: C, -} - -#[derive(Debug, Copy, Clone)] -enum Action { - Create, - Update, - Import, -} - -impl Display for Action { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Action::Create => "Creating", - Action::Update => "Updating", - Action::Import => "Importing", - }; - write!(f, "{}", str) - } -} - -async fn create_or_update_api_definition< - C: golem_client::api::ApiDefinitionClient + Sync + Send, ->( - action: Action, - client: &C, - path: PathBufOrStdin, -) -> Result { - info!("{action} api definition from {path:?}"); - - let definition_str: String = match path { - PathBufOrStdin::Path(path) => read_to_string(path) - .await - .map_err(|e| GolemError(format!("Failed to read from file: {e:?}")))?, - PathBufOrStdin::Stdin => { - let mut content = String::new(); - - let _ = std::io::stdin() - .read_to_string(&mut content) - .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; - - content - } - }; - - match action { - Action::Import => { - let value: serde_json::value::Value = serde_json::from_str(definition_str.as_str()) - .map_err(|e| GolemError(format!("Failed to parse json: {e:?}")))?; - - Ok(client.import_open_api(&value).await?) - } - Action::Create => { - let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) - .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; - - Ok(client.create_definition(&value).await?) - } - Action::Update => { - let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) - .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; - - Ok(client - .update_definition(&value.id, &value.version, &value) - .await?) - } - } -} - -#[async_trait] -impl ApiDefinitionClient - for ApiDefinitionClientLive -{ - async fn list( + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; + async fn update( &self, - id: Option<&ApiDefinitionId>, - ) -> Result, GolemError> { - info!("Getting api definitions"); - - Ok(self - .client - .list_definitions(id.map(|id| id.0.as_str())) - .await?) - } - - async fn get( + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; + async fn import( &self, - id: ApiDefinitionId, - version: ApiDefinitionVersion, - ) -> Result { - info!("Getting api definition for {}/{}", id.0, version.0); - - Ok(self - .client - .get_definition(id.0.as_str(), version.0.as_str()) - .await?) - } - - async fn create(&self, path: PathBufOrStdin) -> Result { - create_or_update_api_definition(Action::Create, &self.client, path).await - } - - async fn update(&self, path: PathBufOrStdin) -> Result { - create_or_update_api_definition(Action::Update, &self.client, path).await - } - - async fn import(&self, path: PathBufOrStdin) -> Result { - create_or_update_api_definition(Action::Import, &self.client, path).await - } - + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; async fn delete( &self, id: ApiDefinitionId, version: ApiDefinitionVersion, - ) -> Result { - info!("Deleting api definition for {}/{}", id.0, version.0); - Ok(self - .client - .delete_definition(id.0.as_str(), version.0.as_str()) - .await?) - } + project: &Self::ProjectContext, + ) -> Result; } diff --git a/golem-cli/src/clients/api_deployment.rs b/golem-cli/src/clients/api_deployment.rs index 1f31c98f5..6179933d1 100644 --- a/golem-cli/src/clients/api_deployment.rs +++ b/golem-cli/src/clients/api_deployment.rs @@ -12,83 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, ApiDeployment, GolemError}; use async_trait::async_trait; -use golem_client::model::{ApiDeployment, ApiSite}; -use tracing::info; - -use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError}; - #[async_trait] pub trait ApiDeploymentClient { + type ProjectContext; + async fn deploy( &self, api_definition_id: &ApiDefinitionId, version: &ApiDefinitionVersion, host: &str, subdomain: Option, + project: &Self::ProjectContext, ) -> Result; async fn list( &self, api_definition_id: &ApiDefinitionId, + project: &Self::ProjectContext, ) -> Result, GolemError>; async fn get(&self, site: &str) -> Result; async fn delete(&self, site: &str) -> Result; } - -#[derive(Clone)] -pub struct ApiDeploymentClientLive { - pub client: C, -} - -#[async_trait] -impl ApiDeploymentClient - for ApiDeploymentClientLive -{ - async fn deploy( - &self, - api_definition_id: &ApiDefinitionId, - version: &ApiDefinitionVersion, - host: &str, - subdomain: Option, - ) -> Result { - info!( - "Deploying definition {api_definition_id}/{version}, host {host} {}", - subdomain - .clone() - .map_or("".to_string(), |s| format!("subdomain {}", s)) - ); - - let deployment = ApiDeployment { - api_definition_id: api_definition_id.0.to_string(), - version: version.0.to_string(), - site: ApiSite { - host: host.to_string(), - subdomain, - }, - }; - - Ok(self.client.deploy(&deployment).await?) - } - - async fn list( - &self, - api_definition_id: &ApiDefinitionId, - ) -> Result, GolemError> { - info!("List api deployments with definition {api_definition_id}"); - - Ok(self.client.list_deployments(&api_definition_id.0).await?) - } - - async fn get(&self, site: &str) -> Result { - info!("Getting api deployment for site {site}"); - - Ok(self.client.get_deployment(site).await?) - } - - async fn delete(&self, site: &str) -> Result { - info!("Deleting api deployment for site {site}"); - - Ok(self.client.delete_deployment(site).await?) - } -} diff --git a/golem-cli/src/clients/component.rs b/golem-cli/src/clients/component.rs index 1ab09421b..81bd20c1d 100644 --- a/golem-cli/src/clients/component.rs +++ b/golem-cli/src/clients/component.rs @@ -12,18 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::io::Read; - -use async_trait::async_trait; -use golem_client::model::Component; - -use tokio::fs::File; -use tracing::info; - +use crate::model::component::Component; use crate::model::{ComponentId, ComponentName, GolemError, PathBufOrStdin}; +use async_trait::async_trait; #[async_trait] pub trait ComponentClient { + type ProjectContext; + async fn get_metadata( &self, component_id: &ComponentId, @@ -33,105 +29,16 @@ pub trait ComponentClient { &self, component_id: &ComponentId, ) -> Result; - async fn find(&self, name: Option) -> Result, GolemError>; - async fn add(&self, name: ComponentName, file: PathBufOrStdin) - -> Result; - async fn update(&self, id: ComponentId, file: PathBufOrStdin) -> Result; -} - -#[derive(Clone)] -pub struct ComponentClientLive { - pub client: C, -} - -#[async_trait] -impl ComponentClient - for ComponentClientLive -{ - async fn get_metadata( - &self, - component_id: &ComponentId, - version: u64, - ) -> Result { - info!("Getting component version"); - - Ok(self - .client - .get_component_metadata(&component_id.0, &version.to_string()) - .await?) - } - - async fn get_latest_metadata( + async fn find( &self, - component_id: &ComponentId, - ) -> Result { - info!("Getting latest component version"); - - Ok(self - .client - .get_latest_component_metadata(&component_id.0) - .await?) - } - - async fn find(&self, name: Option) -> Result, GolemError> { - info!("Getting components"); - - let name = name.map(|n| n.0); - - Ok(self.client.get_components(name.as_deref()).await?) - } - + name: Option, + project: &Option, + ) -> Result, GolemError>; async fn add( &self, name: ComponentName, - path: PathBufOrStdin, - ) -> Result { - info!("Adding component {name:?} from {path:?}"); - - let component = match path { - PathBufOrStdin::Path(path) => { - let file = File::open(path) - .await - .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; - - self.client.create_component(&name.0, file).await? - } - PathBufOrStdin::Stdin => { - let mut bytes = Vec::new(); - - let _ = std::io::stdin() - .read_to_end(&mut bytes) // TODO: steaming request from stdin - .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; - - self.client.create_component(&name.0, bytes).await? - } - }; - - Ok(component) - } - - async fn update(&self, id: ComponentId, path: PathBufOrStdin) -> Result { - info!("Updating component {id:?} from {path:?}"); - - let component = match path { - PathBufOrStdin::Path(path) => { - let file = File::open(path) - .await - .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; - - self.client.update_component(&id.0, file).await? - } - PathBufOrStdin::Stdin => { - let mut bytes = Vec::new(); - - let _ = std::io::stdin() - .read_to_end(&mut bytes) - .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; - - self.client.update_component(&id.0, bytes).await? - } - }; - - Ok(component) - } + file: PathBufOrStdin, + project: &Option, + ) -> Result; + async fn update(&self, id: ComponentId, file: PathBufOrStdin) -> Result; } diff --git a/golem-cli/src/clients/health_check.rs b/golem-cli/src/clients/health_check.rs index 9a4430568..5ed06e810 100644 --- a/golem-cli/src/clients/health_check.rs +++ b/golem-cli/src/clients/health_check.rs @@ -12,29 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::model::GolemError; use async_trait::async_trait; use golem_client::model::VersionInfo; -use tracing::debug; - -use crate::model::GolemError; #[async_trait] pub trait HealthCheckClient { async fn version(&self) -> Result; } - -#[derive(Clone)] -pub struct HealthCheckClientLive { - pub client: C, -} - -#[async_trait] -impl HealthCheckClient - for HealthCheckClientLive -{ - async fn version(&self) -> Result { - debug!("Getting server version"); - - Ok(self.client.version().await?) - } -} diff --git a/golem-cli/src/clients/worker.rs b/golem-cli/src/clients/worker.rs index b454434a8..247b950aa 100644 --- a/golem-cli/src/clients/worker.rs +++ b/golem-cli/src/clients/worker.rs @@ -12,25 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Duration; - -use async_trait::async_trait; -use futures_util::{future, pin_mut, SinkExt, StreamExt}; -use golem_client::model::{ - CallingConvention, InvokeParameters, InvokeResult, ScanCursor, UpdateWorkerRequest, - WorkerCreationRequest, WorkerFilter, WorkerId, WorkerMetadata, WorkersMetadataRequest, +use crate::model::{ + ComponentId, GolemError, IdempotencyKey, WorkerMetadata, WorkerName, WorkerUpdateMode, WorkersMetadataResponse, }; -use golem_client::Context; -use native_tls::TlsConnector; -use serde::Deserialize; -use tokio::{task, time}; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::protocol::Message; -use tokio_tungstenite::{connect_async_tls_with_config, Connector}; -use tracing::{debug, info}; - -use crate::model::{ComponentId, GolemError, IdempotencyKey, WorkerName, WorkerUpdateMode}; +use async_trait::async_trait; +use golem_client::model::{InvokeParameters, InvokeResult, ScanCursor, WorkerFilter, WorkerId}; #[async_trait] pub trait WorkerClient { @@ -103,415 +90,3 @@ pub trait WorkerClient { target_version: u64, ) -> Result<(), GolemError>; } - -#[derive(Clone)] -pub struct WorkerClientLive { - pub client: C, - pub context: Context, - pub allow_insecure: bool, -} - -#[async_trait] -impl WorkerClient for WorkerClientLive { - async fn new_worker( - &self, - name: WorkerName, - component_id: ComponentId, - args: Vec, - env: Vec<(String, String)>, - ) -> Result { - info!("Creating worker {name} of {}", component_id.0); - - Ok(self - .client - .launch_new_worker( - &component_id.0, - &WorkerCreationRequest { - name: name.0, - args, - env: env.into_iter().collect(), - }, - ) - .await? - .worker_id) - } - - async fn invoke_and_await( - &self, - name: WorkerName, - component_id: ComponentId, - function: String, - parameters: InvokeParameters, - idempotency_key: Option, - use_stdio: bool, - ) -> Result { - info!( - "Invoke and await for function {function} in {}/{}", - component_id.0, name.0 - ); - - let calling_convention = if use_stdio { - CallingConvention::Stdio - } else { - CallingConvention::Component - }; - - Ok(self - .client - .invoke_and_await_function( - &component_id.0, - &name.0, - idempotency_key.as_ref().map(|k| k.0.as_str()), - &function, - Some(&calling_convention), - ¶meters, - ) - .await?) - } - - async fn invoke( - &self, - name: WorkerName, - component_id: ComponentId, - function: String, - parameters: InvokeParameters, - idempotency_key: Option, - ) -> Result<(), GolemError> { - info!( - "Invoke function {function} in {}/{}", - component_id.0, name.0 - ); - - let _ = self - .client - .invoke_function( - &component_id.0, - &name.0, - idempotency_key.as_ref().map(|k| k.0.as_str()), - &function, - ¶meters, - ) - .await?; - Ok(()) - } - - async fn interrupt( - &self, - name: WorkerName, - component_id: ComponentId, - ) -> Result<(), GolemError> { - info!("Interrupting {}/{}", component_id.0, name.0); - - let _ = self - .client - .interrupt_worker(&component_id.0, &name.0, Some(false)) - .await?; - Ok(()) - } - - async fn simulated_crash( - &self, - name: WorkerName, - component_id: ComponentId, - ) -> Result<(), GolemError> { - info!("Simulating crash of {}/{}", component_id.0, name.0); - - let _ = self - .client - .interrupt_worker(&component_id.0, &name.0, Some(true)) - .await?; - Ok(()) - } - - async fn delete(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { - info!("Deleting worker {}/{}", component_id.0, name.0); - - let _ = self.client.delete_worker(&component_id.0, &name.0).await?; - Ok(()) - } - - async fn get_metadata( - &self, - name: WorkerName, - component_id: ComponentId, - ) -> Result { - info!("Getting worker {}/{} metadata", component_id.0, name.0); - - Ok(self - .client - .get_worker_metadata(&component_id.0, &name.0) - .await?) - } - - async fn find_metadata( - &self, - component_id: ComponentId, - filter: Option, - cursor: Option, - count: Option, - precise: Option, - ) -> Result { - info!( - "Getting workers metadata for component: {}, filter: {}", - component_id.0, - filter.is_some() - ); - - Ok(self - .client - .find_workers_metadata( - &component_id.0, - &WorkersMetadataRequest { - filter, - cursor, - count, - precise, - }, - ) - .await?) - } - - async fn list_metadata( - &self, - component_id: ComponentId, - filter: Option>, - cursor: Option, - count: Option, - precise: Option, - ) -> Result { - info!( - "Getting workers metadata for component: {}, filter: {}", - component_id.0, - filter - .clone() - .map(|fs| fs.join(" AND ")) - .unwrap_or("N/A".to_string()) - ); - - let filter: Option<&[String]> = filter.as_deref(); - - match cursor { - None => Ok(self - .client - .get_workers_metadata(&component_id.0, filter, None, count, precise) - .await?), - Some(cursor) => { - let cursor = format!("{}/{}", cursor.layer, cursor.cursor); - Ok(self - .client - .get_workers_metadata(&component_id.0, filter, Some(&cursor), count, precise) - .await?) - } - } - } - - async fn connect(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { - let mut url = self.context.base_url.clone(); - - let ws_schema = if url.scheme() == "http" { "ws" } else { "wss" }; - - url.set_scheme(ws_schema) - .map_err(|_| GolemError("Can't set schema.".to_string()))?; - - url.path_segments_mut() - .map_err(|_| GolemError("Can't get path.".to_string()))? - .push("v2") - .push("components") - .push(&component_id.0.to_string()) - .push("workers") - .push(&name.0) - .push("connect"); - - let mut request = url - .into_client_request() - .map_err(|e| GolemError(format!("Can't create request: {e}")))?; - let headers = request.headers_mut(); - - if let Some(token) = self.context.bearer_token() { - headers.insert( - "Authorization", - format!("Bearer {}", token).parse().unwrap(), - ); - } - - let connector = if self.allow_insecure { - Some(Connector::NativeTls( - TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - .unwrap(), - )) - } else { - None - }; - - let (ws_stream, _) = connect_async_tls_with_config(request, None, false, connector) - .await - .map_err(|e| match e { - tungstenite::error::Error::Http(http_error_response) => { - match http_error_response.body().clone() { - Some(body) => GolemError(format!( - "Failed Websocket. Http error: {}, {}", - http_error_response.status(), - String::from_utf8_lossy(&body) - )), - None => GolemError(format!( - "Failed Websocket. Http error: {}", - http_error_response.status() - )), - } - } - _ => GolemError(format!("Failed Websocket. Error: {}", e)), - })?; - - let (mut write, read) = ws_stream.split(); - - let pings = task::spawn(async move { - let mut interval = time::interval(Duration::from_secs(5)); // TODO configure - - let mut cnt: i32 = 1; - - loop { - interval.tick().await; - - write - .send(Message::Ping(cnt.to_ne_bytes().to_vec())) - .await - .unwrap(); // TODO: handle errors: map_err(|e| GolemError(format!("Ping failure: {e}")))?; - - cnt += 1; - } - }); - - let read_res = read.for_each(|message_or_error| async { - match message_or_error { - Err(error) => { - print!("Error reading message: {}", error); - } - Ok(message) => { - let instance_connect_msg = match message { - Message::Text(str) => { - let parsed: serde_json::Result = - serde_json::from_str(&str); - Some(parsed.unwrap()) // TODO: error handling - } - Message::Binary(data) => { - let parsed: serde_json::Result = - serde_json::from_slice(&data); - Some(parsed.unwrap()) // TODO: error handling - } - Message::Ping(_) => { - debug!("Ignore ping"); - None - } - Message::Pong(_) => { - debug!("Ignore pong"); - None - } - Message::Close(details) => { - match details { - Some(closed_frame) => { - print!("Connection Closed: {}", closed_frame); - } - None => { - print!("Connection Closed"); - } - } - None - } - Message::Frame(_) => { - info!("Ignore unexpected frame"); - None - } - }; - - match instance_connect_msg { - None => {} - Some(msg) => match msg.event { - WorkerEvent::Stdout(StdOutLog { message }) => { - print!("{message}") - } - WorkerEvent::Stderr(StdErrLog { message }) => { - print!("{message}") - } - WorkerEvent::Log(Log { - level, - context, - message, - }) => match level { - 0 => tracing::trace!(message, context = context), - 1 => tracing::debug!(message, context = context), - 2 => tracing::info!(message, context = context), - 3 => tracing::warn!(message, context = context), - _ => tracing::error!(message, context = context), - }, - }, - } - } - } - }); - - pin_mut!(read_res, pings); - - future::select(pings, read_res).await; - - Ok(()) - } - - async fn update( - &self, - name: WorkerName, - component_id: ComponentId, - mode: WorkerUpdateMode, - target_version: u64, - ) -> Result<(), GolemError> { - info!("Updating worker {name} of {}", component_id.0); - let update_mode = match mode { - WorkerUpdateMode::Automatic => golem_client::model::WorkerUpdateMode::Automatic, - WorkerUpdateMode::Manual => golem_client::model::WorkerUpdateMode::Manual, - }; - - let _ = self - .client - .update_worker( - &component_id.0, - &name.0, - &UpdateWorkerRequest { - mode: update_mode, - target_version, - }, - ) - .await?; - Ok(()) - } -} - -#[derive(Deserialize, Debug)] -struct InstanceConnectMessage { - pub event: WorkerEvent, -} - -#[derive(Deserialize, Debug)] -enum WorkerEvent { - Stdout(StdOutLog), - Stderr(StdErrLog), - Log(Log), -} - -#[derive(Deserialize, Debug)] -struct StdOutLog { - message: String, -} - -#[derive(Deserialize, Debug)] -struct StdErrLog { - message: String, -} - -#[derive(Deserialize, Debug)] -struct Log { - pub level: i32, - pub context: String, - pub message: String, -} diff --git a/golem-cli/src/cloud.rs b/golem-cli/src/cloud.rs new file mode 100644 index 000000000..b05871cd0 --- /dev/null +++ b/golem-cli/src/cloud.rs @@ -0,0 +1,20 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod auth; +pub mod clients; +pub mod command; +pub mod factory; +pub mod model; +pub mod service; diff --git a/golem-cli/src/cloud/auth.rs b/golem-cli/src/cloud/auth.rs new file mode 100644 index 000000000..08b82dda6 --- /dev/null +++ b/golem-cli/src/cloud/auth.rs @@ -0,0 +1,196 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs::{create_dir_all, File, OpenOptions}; +use std::io::{BufReader, BufWriter}; +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use golem_cloud_client::model::{OAuth2Data, Token, TokenSecret, UnsafeToken}; +use indoc::printdoc; +use serde::{Deserialize, Serialize}; +use tracing::info; +use uuid::Uuid; + +use crate::cloud::clients::login::LoginClient; +use crate::cloud::clients::CloudAuthentication; +use crate::model::GolemError; + +#[async_trait] +pub trait Auth { + async fn authenticate( + &self, + manual_token: Option, + config_dir: PathBuf, + ) -> Result; +} + +pub struct AuthLive { + pub login: Box, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CloudAuthenticationConfig { + data: CloudAuthenticationConfigData, + secret: Uuid, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CloudAuthenticationConfigData { + id: Uuid, + account_id: String, + created_at: DateTime, + expires_at: DateTime, +} + +impl AuthLive { + fn read_from_file(&self, config_dir: &Path) -> Option { + let file = File::open(self.config_path(config_dir)).ok()?; // TODO log + + let reader = BufReader::new(file); + + let parsed: serde_json::Result = serde_json::from_reader(reader); + + match parsed { + Ok(conf) => Some(CloudAuthentication(UnsafeToken { + data: Token { + id: conf.data.id, + account_id: conf.data.account_id, + created_at: conf.data.created_at, + expires_at: conf.data.expires_at, + }, + secret: TokenSecret { value: conf.secret }, + })), + Err(err) => { + info!("Parsing failed: {err}"); // TODO configure + None + } + } + } + + fn config_path(&self, config_dir: &Path) -> PathBuf { + config_dir.join("cloud_authentication.json") + } + + fn store_file(&self, token: &UnsafeToken, config_dir: &Path) { + match create_dir_all(config_dir) { + Ok(_) => {} + Err(err) => { + info!("Can't create config directory: {err}"); + } + } + let file_res = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(true) + .open(self.config_path(config_dir)); + let file = match file_res { + Ok(file) => file, + Err(err) => { + info!("Can't open file: {err}"); + return; + } + }; + let writer = BufWriter::new(file); + let data = CloudAuthenticationConfig { + data: CloudAuthenticationConfigData { + id: token.data.id, + account_id: token.data.account_id.clone(), + created_at: token.data.created_at, + expires_at: token.data.expires_at, + }, + secret: token.secret.value, + }; + let res = serde_json::to_writer_pretty(writer, &data); + + if let Err(err) = res { + info!("File sawing error: {err}"); + } + } + + async fn oauth2(&self, config_dir: &Path) -> Result { + let data = self.login.start_oauth2().await?; + inform_user(&data); + let token = self.login.complete_oauth2(data.encoded_session).await?; + self.store_file(&token, config_dir); + Ok(CloudAuthentication(token)) + } + + async fn config_authentication( + &self, + config_dir: PathBuf, + ) -> Result { + if let Some(data) = self.read_from_file(&config_dir) { + Ok(data) + } else { + self.oauth2(&config_dir).await + } + } +} + +fn inform_user(data: &OAuth2Data) { + let box_url_line = String::from_utf8(vec![b'-'; data.url.len() + 2]).unwrap(); + let box_code_line = String::from_utf8(vec![b'-'; data.user_code.len() + 2]).unwrap(); + let expires: DateTime = data.expires; + let expires_in = expires.signed_duration_since(Utc::now()).num_minutes(); + let expires_at = expires.format("%T"); + let url = &data.url; + let user_code = &data.user_code; + + printdoc! {" + >> + >> Application requests to perform OAuth2 + >> authorization. + >> + >> Visit following URL in a browser: + >> + >> ┏{box_url_line}┓ + >> ┃ {url} ┃ + >> ┗{box_url_line}┛ + >> + >> And enter following code: + >> + >> ┏{box_code_line}┓ + >> ┃ {user_code} ┃ + >> ┗{box_code_line}┛ + >> + >> Code will expire in {expires_in} minutes at {expires_at}. + >> + Waiting... + "} +} + +#[async_trait] +impl Auth for AuthLive { + async fn authenticate( + &self, + manual_token: Option, + config_dir: PathBuf, + ) -> Result { + if let Some(manual_token) = manual_token { + let secret = TokenSecret { + value: manual_token, + }; + let data = self.login.token_details(secret.clone()).await?; + + Ok(CloudAuthentication(UnsafeToken { data, secret })) + } else { + self.config_authentication(config_dir).await + } + } +} diff --git a/golem-cli/src/cloud/clients.rs b/golem-cli/src/cloud/clients.rs new file mode 100644 index 000000000..4a8e74580 --- /dev/null +++ b/golem-cli/src/cloud/clients.rs @@ -0,0 +1,80 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod account; +pub mod component; +pub mod errors; +pub mod gateway; +pub mod grant; +pub mod health_check; +pub mod login; +pub mod policy; +pub mod project; +pub mod project_grant; +pub mod token; +pub mod worker; + +use crate::cloud::model::{AccountId, ProjectAction}; +use golem_cloud_client::model::{TokenSecret, UnsafeToken}; + +pub fn token_header(secret: &TokenSecret) -> String { + format!("bearer {}", secret.value) +} + +#[derive(Clone, PartialEq, Debug)] +pub struct CloudAuthentication(pub UnsafeToken); + +impl CloudAuthentication { + pub fn header(&self) -> String { + let CloudAuthentication(value) = self; + + token_header(&value.secret) + } + + pub fn account_id(&self) -> AccountId { + let CloudAuthentication(value) = self; + + AccountId { + id: value.data.account_id.clone(), + } + } +} + +pub fn action_cli_to_api(action: ProjectAction) -> golem_cloud_client::model::ProjectAction { + match action { + ProjectAction::ViewComponent => golem_cloud_client::model::ProjectAction::ViewComponent {}, + ProjectAction::CreateComponent => { + golem_cloud_client::model::ProjectAction::CreateComponent {} + } + ProjectAction::UpdateComponent => { + golem_cloud_client::model::ProjectAction::UpdateComponent {} + } + ProjectAction::DeleteComponent => { + golem_cloud_client::model::ProjectAction::DeleteComponent {} + } + ProjectAction::ViewWorker => golem_cloud_client::model::ProjectAction::ViewWorker {}, + ProjectAction::CreateWorker => golem_cloud_client::model::ProjectAction::CreateWorker {}, + ProjectAction::UpdateWorker => golem_cloud_client::model::ProjectAction::UpdateWorker {}, + ProjectAction::DeleteWorker => golem_cloud_client::model::ProjectAction::DeleteWorker {}, + ProjectAction::ViewProjectGrants => { + golem_cloud_client::model::ProjectAction::ViewProjectGrants {} + } + ProjectAction::CreateProjectGrants => { + golem_cloud_client::model::ProjectAction::CreateProjectGrants {} + } + ProjectAction::DeleteProjectGrants => { + golem_cloud_client::model::ProjectAction::DeleteProjectGrants {} + } + } +} diff --git a/golem-cli/src/cloud/clients/account.rs b/golem-cli/src/cloud/clients/account.rs new file mode 100644 index 000000000..7d8701e25 --- /dev/null +++ b/golem-cli/src/cloud/clients/account.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::AccountId; +use async_trait::async_trait; +use golem_cloud_client::model::{Account, AccountData, Plan}; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait AccountClient { + async fn get(&self, id: &AccountId) -> Result; + async fn get_plan(&self, id: &AccountId) -> Result; + async fn put(&self, id: &AccountId, data: AccountData) -> Result; + async fn post(&self, data: AccountData) -> Result; + async fn delete(&self, id: &AccountId) -> Result<(), GolemError>; +} + +pub struct AccountClientLive { + pub client: C, +} + +#[async_trait] +impl AccountClient + for AccountClientLive +{ + async fn get(&self, id: &AccountId) -> Result { + info!("Getting account {id}"); + Ok(self.client.account_id_get(&id.id).await?) + } + + async fn get_plan(&self, id: &AccountId) -> Result { + info!("Getting account plan of {id}."); + Ok(self.client.account_id_plan_get(&id.id).await?) + } + + async fn put(&self, id: &AccountId, data: AccountData) -> Result { + info!("Updating account {id}."); + Ok(self.client.account_id_put(&id.id, &data).await?) + } + + async fn post(&self, data: AccountData) -> Result { + info!("Creating account."); + Ok(self.client.post(&data).await?) + } + + async fn delete(&self, id: &AccountId) -> Result<(), GolemError> { + info!("Deleting account {id}."); + let _ = self.client.account_id_delete(&id.id).await?; + Ok(()) + } +} diff --git a/golem-cli/src/cloud/clients/component.rs b/golem-cli/src/cloud/clients/component.rs new file mode 100644 index 000000000..22d7fc3da --- /dev/null +++ b/golem-cli/src/cloud/clients/component.rs @@ -0,0 +1,140 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Read; + +use crate::clients::component::ComponentClient; +use crate::cloud::model::ProjectId; +use async_trait::async_trait; +use golem_cloud_client::model::ComponentQuery; +use tokio::fs::File; +use tracing::info; + +use crate::model::component::Component; +use crate::model::{ComponentId, ComponentName, GolemError, PathBufOrStdin}; + +#[derive(Debug, Clone)] +pub struct ComponentClientLive { + pub client: C, +} + +#[async_trait] +impl ComponentClient + for ComponentClientLive +{ + type ProjectContext = ProjectId; + + async fn get_metadata( + &self, + component_id: &ComponentId, + version: u64, + ) -> Result { + info!("Getting component version"); + let component = self + .client + .get_component_metadata(&component_id.0, &version.to_string()) + .await?; + Ok(component.into()) + } + + async fn get_latest_metadata( + &self, + component_id: &ComponentId, + ) -> Result { + info!("Getting latest component version"); + + let component = self + .client + .get_latest_component_metadata(&component_id.0) + .await?; + Ok(component.into()) + } + + async fn find( + &self, + name: Option, + project: &Option, + ) -> Result, GolemError> { + info!("Getting components"); + + let project_id = project.map(|p| p.0); + let name = name.map(|n| n.0); + + let components = self + .client + .get_components(project_id.as_ref(), name.as_deref()) + .await?; + Ok(components.into_iter().map(|c| c.into()).collect()) + } + + async fn add( + &self, + name: ComponentName, + file: PathBufOrStdin, + project: &Option, + ) -> Result { + info!("Adding component {name:?} from {file:?}"); + + let query = ComponentQuery { + project_id: project.map(|ProjectId(id)| id), + component_name: name.0, + }; + + let component = match file { + PathBufOrStdin::Path(path) => { + let file = File::open(path) + .await + .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; + + self.client.create_component(&query, file).await? + } + PathBufOrStdin::Stdin => { + let mut bytes = Vec::new(); + + let _ = std::io::stdin() + .read_to_end(&mut bytes) // TODO: steaming request from stdin + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + self.client.create_component(&query, bytes).await? + } + }; + + Ok(component.into()) + } + + async fn update(&self, id: ComponentId, file: PathBufOrStdin) -> Result { + info!("Updating component {id:?} from {file:?}"); + + let component = match file { + PathBufOrStdin::Path(path) => { + let file = File::open(path) + .await + .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; + + self.client.update_component(&id.0, file).await? + } + PathBufOrStdin::Stdin => { + let mut bytes = Vec::new(); + + let _ = std::io::stdin() + .read_to_end(&mut bytes) // TODO: steaming request from stdin + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + self.client.update_component(&id.0, bytes).await? + } + }; + + Ok(component.into()) + } +} diff --git a/golem-cli/src/cloud/clients/errors.rs b/golem-cli/src/cloud/clients/errors.rs new file mode 100644 index 000000000..489cbd4ef --- /dev/null +++ b/golem-cli/src/cloud/clients/errors.rs @@ -0,0 +1,214 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::ResponseContentErrorMapper; +use golem_cloud_client::api::{ + AccountError, ComponentError, GrantError, HealthCheckError, LoginError, ProjectError, + ProjectGrantError, ProjectPolicyError, TokenError, +}; +use golem_cloud_worker_client::api::WorkerError; + +impl ResponseContentErrorMapper for AccountError { + fn map(self) -> String { + match self { + AccountError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + AccountError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + AccountError::Error404(error) => { + format!("NotFound: {error:?}") + } + AccountError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for GrantError { + fn map(self) -> String { + match self { + GrantError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + GrantError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + GrantError::Error404(error) => { + format!("NotFound: {error:?}") + } + GrantError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for LoginError { + fn map(self) -> String { + match self { + LoginError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + LoginError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + LoginError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for ProjectError { + fn map(self) -> String { + match self { + ProjectError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ProjectError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ProjectError::Error403(error) => { + format!("Forbidden: {error:?}") + } + ProjectError::Error404(error) => { + format!("NotFound: {error:?}") + } + ProjectError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for ProjectGrantError { + fn map(self) -> String { + match self { + ProjectGrantError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ProjectGrantError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ProjectGrantError::Error403(error) => { + format!("Forbidden: {error:?}") + } + ProjectGrantError::Error404(error) => { + format!("NotFound: {error:?}") + } + ProjectGrantError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +#[allow(unreachable_patterns)] +impl ResponseContentErrorMapper for ProjectPolicyError { + fn map(self) -> String { + match self { + ProjectPolicyError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ProjectPolicyError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ProjectPolicyError::Error404(error) => { + format!("NotFound: {error:?}") + } + ProjectPolicyError::Error500(error) => { + format!("InternalError: {error:?}") + } + _ => "UnknownError".into(), + } + } +} + +impl ResponseContentErrorMapper for ComponentError { + fn map(self) -> String { + match self { + ComponentError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ComponentError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ComponentError::Error403(error) => { + format!("Forbidden: {error:?}") + } + ComponentError::Error404(error) => { + format!("NotFound: {error:?}") + } + ComponentError::Error409(error) => { + format!("Conflict: {error:?}") + } + ComponentError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for TokenError { + fn map(self) -> String { + match self { + TokenError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + TokenError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + TokenError::Error404(error) => { + format!("NotFound: {error:?}") + } + TokenError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for WorkerError { + fn map(self) -> String { + match self { + WorkerError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + WorkerError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + WorkerError::Error403(error) => { + format!("Forbidden: {error:?}") + } + WorkerError::Error404(error) => { + format!("NotFound: {error:?}") + } + WorkerError::Error409(error) => { + format!("Conflict: {error:?}") + } + WorkerError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for HealthCheckError { + fn map(self) -> String { + match self {} + } +} diff --git a/golem-cli/src/cloud/clients/gateway.rs b/golem-cli/src/cloud/clients/gateway.rs new file mode 100644 index 000000000..64b4a3497 --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway.rs @@ -0,0 +1,20 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod api_definition; +pub mod api_deployment; +pub mod certificate; +pub mod domain; +pub mod errors; +pub mod health_check; diff --git a/golem-cli/src/cloud/clients/gateway/api_definition.rs b/golem-cli/src/cloud/clients/gateway/api_definition.rs new file mode 100644 index 000000000..ce3fcec78 --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/api_definition.rs @@ -0,0 +1,232 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Display; + +use std::io::Read; + +use async_trait::async_trait; + +use golem_cloud_worker_client::model::{ + GolemWorkerBinding, HttpApiDefinition, MethodPattern, Route, +}; + +use crate::clients::api_definition::ApiDefinitionClient; +use crate::cloud::model::ProjectId; +use tokio::fs::read_to_string; +use tracing::info; + +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, PathBufOrStdin}; + +#[derive(Clone)] +pub struct ApiDefinitionClientLive< + C: golem_cloud_worker_client::api::ApiDefinitionClient + Sync + Send, +> { + pub client: C, +} + +#[derive(Debug, Copy, Clone)] +enum Action { + Create, + Update, + Import, +} + +impl Display for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Action::Create => "Creating", + Action::Update => "Updating", + Action::Import => "Importing", + }; + write!(f, "{}", str) + } +} + +async fn create_or_update_api_definition< + C: golem_cloud_worker_client::api::ApiDefinitionClient + Sync + Send, +>( + action: Action, + client: &C, + path: PathBufOrStdin, + project_id: &ProjectId, +) -> Result { + info!("{action} api definition from {path:?}"); + + let definition_str: String = match path { + PathBufOrStdin::Path(path) => read_to_string(path) + .await + .map_err(|e| GolemError(format!("Failed to read from file: {e:?}")))?, + PathBufOrStdin::Stdin => { + let mut content = String::new(); + + let _ = std::io::stdin() + .read_to_string(&mut content) + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + content + } + }; + + let res = match action { + Action::Import => { + let value: serde_json::value::Value = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse json: {e:?}")))?; + + client.import_open_api(&project_id.0, &value).await? + } + Action::Create => { + let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; + + client.create_definition(&project_id.0, &value).await? + } + Action::Update => { + let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; + + client + .update_definition(&project_id.0, &value.id, &value.version, &value) + .await? + } + }; + + Ok(to_oss_http_api_definition(res)) +} + +fn to_oss_method_pattern(p: MethodPattern) -> golem_client::model::MethodPattern { + match p { + MethodPattern::Get => golem_client::model::MethodPattern::Get, + MethodPattern::Connect => golem_client::model::MethodPattern::Connect, + MethodPattern::Post => golem_client::model::MethodPattern::Post, + MethodPattern::Delete => golem_client::model::MethodPattern::Delete, + MethodPattern::Put => golem_client::model::MethodPattern::Put, + MethodPattern::Patch => golem_client::model::MethodPattern::Patch, + MethodPattern::Options => golem_client::model::MethodPattern::Options, + MethodPattern::Trace => golem_client::model::MethodPattern::Trace, + MethodPattern::Head => golem_client::model::MethodPattern::Head, + } +} + +fn to_oss_golem_worker_binding(_b: GolemWorkerBinding) -> golem_client::model::GolemWorkerBinding { + todo!("Migrate new bindings to cloud") +} + +fn to_oss_route(r: Route) -> golem_client::model::Route { + let Route { + method, + path, + binding, + } = r; + + golem_client::model::Route { + method: to_oss_method_pattern(method), + path, + binding: to_oss_golem_worker_binding(binding), + } +} + +fn to_oss_http_api_definition(d: HttpApiDefinition) -> golem_client::model::HttpApiDefinition { + let HttpApiDefinition { + id, + version, + routes, + draft, + } = d; + + golem_client::model::HttpApiDefinition { + id, + version, + routes: routes.into_iter().map(to_oss_route).collect(), + draft, + } +} + +#[async_trait] +impl ApiDefinitionClient + for ApiDefinitionClientLive +{ + type ProjectContext = ProjectId; + + async fn list( + &self, + id: Option<&ApiDefinitionId>, + project: &Self::ProjectContext, + ) -> Result, GolemError> { + info!("Getting api definitions"); + + let definitions = self + .client + .list_definitions(&project.0, id.map(|id| id.0.as_str())) + .await?; + + Ok(definitions + .into_iter() + .map(to_oss_http_api_definition) + .collect()) + } + + async fn get( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result { + info!("Getting api definition for {}/{}", id.0, version.0); + + let definition = self + .client + .get_definition(&project.0, id.0.as_str(), version.0.as_str()) + .await?; + + Ok(to_oss_http_api_definition(definition)) + } + + async fn create( + &self, + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Create, &self.client, path, project).await + } + + async fn update( + &self, + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Update, &self.client, path, project).await + } + + async fn import( + &self, + path: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Import, &self.client, path, project).await + } + + async fn delete( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result { + info!("Deleting api definition for {}/{}", id.0, version.0); + Ok(self + .client + .delete_definition(&project.0, id.0.as_str(), version.0.as_str()) + .await?) + } +} diff --git a/golem-cli/src/cloud/clients/gateway/api_deployment.rs b/golem-cli/src/cloud/clients/gateway/api_deployment.rs new file mode 100644 index 000000000..a95bc8456 --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/api_deployment.rs @@ -0,0 +1,92 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; + +use crate::clients::api_deployment::ApiDeploymentClient; +use crate::cloud::model::ProjectId; +use golem_cloud_worker_client::model::ApiSite; +use itertools::Itertools; +use tracing::info; + +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, ApiDeployment, GolemError}; + +#[derive(Clone)] +pub struct ApiDeploymentClientLive< + C: golem_cloud_worker_client::api::ApiDeploymentClient + Sync + Send, +> { + pub client: C, +} + +#[async_trait] +impl ApiDeploymentClient + for ApiDeploymentClientLive +{ + type ProjectContext = ProjectId; + + async fn deploy( + &self, + api_definition_id: &ApiDefinitionId, + version: &ApiDefinitionVersion, + host: &str, + subdomain: Option, + project: &Self::ProjectContext, + ) -> Result { + info!( + "Deploying definition {api_definition_id}/{version}, host {host} {}", + subdomain + .clone() + .map_or("".to_string(), |s| format!("subdomain {}", s)) + ); + + let deployment = golem_cloud_worker_client::model::ApiDeployment { + api_definition_id: api_definition_id.0.to_string(), + version: version.0.to_string(), + project_id: project.0, + site: ApiSite { + host: host.to_string(), + subdomain: subdomain.expect("Subdomain is mandatory"), // TODO: unify OSS and cloud + }, + }; + + Ok(self.client.deploy(&deployment).await?.into()) + } + + async fn list( + &self, + api_definition_id: &ApiDefinitionId, + project: &Self::ProjectContext, + ) -> Result, GolemError> { + info!("List api deployments with definition {api_definition_id}"); + + let deployments = self + .client + .list_deployments(&project.0, &api_definition_id.0) + .await?; + + Ok(deployments.into_iter().map_into().collect()) + } + + async fn get(&self, site: &str) -> Result { + info!("Getting api deployment for site {site}"); + + Ok(self.client.get_deployment(site).await?.into()) + } + + async fn delete(&self, site: &str) -> Result { + info!("Deleting api deployment for site {site}"); + + Ok(self.client.delete_deployment(site).await?) + } +} diff --git a/golem-cli/src/cloud/clients/gateway/certificate.rs b/golem-cli/src/cloud/clients/gateway/certificate.rs new file mode 100644 index 000000000..acb7b6b5d --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/certificate.rs @@ -0,0 +1,68 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::ProjectId; +use async_trait::async_trait; +use golem_cloud_worker_client::model::{Certificate, CertificateRequest}; +use uuid::Uuid; + +use crate::model::GolemError; + +#[async_trait] +pub trait CertificateClient { + async fn get( + &self, + project_id: ProjectId, + certificate_id: Option<&Uuid>, + ) -> Result, GolemError>; + + async fn create(&self, certificate: CertificateRequest) -> Result; + + async fn delete( + &self, + project_id: ProjectId, + certificate_id: &Uuid, + ) -> Result; +} + +pub struct CertificateClientLive< + C: golem_cloud_worker_client::api::ApiCertificateClient + Sync + Send, +> { + pub client: C, +} + +#[async_trait] +impl CertificateClient + for CertificateClientLive +{ + async fn get( + &self, + project_id: ProjectId, + certificate_id: Option<&Uuid>, + ) -> Result, GolemError> { + Ok(self.client.get(&project_id.0, certificate_id).await?) + } + + async fn create(&self, certificate: CertificateRequest) -> Result { + Ok(self.client.post(&certificate).await?) + } + + async fn delete( + &self, + project_id: ProjectId, + certificate_id: &Uuid, + ) -> Result { + Ok(self.client.delete(&project_id.0, certificate_id).await?) + } +} diff --git a/golem-cli/src/cloud/clients/gateway/domain.rs b/golem-cli/src/cloud/clients/gateway/domain.rs new file mode 100644 index 000000000..d8f3ecea0 --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/domain.rs @@ -0,0 +1,63 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::ProjectId; +use async_trait::async_trait; +use golem_cloud_worker_client::model::{ApiDomain, DomainRequest}; + +use crate::model::GolemError; + +#[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 client: C, +} + +#[async_trait] +impl DomainClient + for DomainClientLive +{ + async fn get(&self, project_id: ProjectId) -> Result, GolemError> { + Ok(self.client.get(&project_id.0).await?) + } + + async fn update( + &self, + project_id: ProjectId, + domain_name: String, + ) -> Result { + Ok(self + .client + .put(&DomainRequest { + project_id: project_id.0, + domain_name, + }) + .await?) + } + + async fn delete(&self, project_id: ProjectId, domain_name: &str) -> Result { + Ok(self.client.delete(&project_id.0, domain_name).await?) + } +} diff --git a/golem-cli/src/cloud/clients/gateway/errors.rs b/golem-cli/src/cloud/clients/gateway/errors.rs new file mode 100644 index 000000000..3b171448d --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/errors.rs @@ -0,0 +1,124 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::ResponseContentErrorMapper; +use golem_cloud_worker_client::api::{ + ApiCertificateError, ApiDefinitionError, ApiDeploymentError, ApiDomainError, HealthCheckError, +}; + +impl ResponseContentErrorMapper for ApiCertificateError { + fn map(self) -> String { + match self { + ApiCertificateError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ApiCertificateError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ApiCertificateError::Error403(error) => { + format!("LimitExceeded: {error:?}") + } + ApiCertificateError::Error404(message) => { + format!("NotFound: {message:?}") + } + ApiCertificateError::Error409(string) => { + format!("AlreadyExists: {string:?}") + } + ApiCertificateError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for ApiDefinitionError { + fn map(self) -> String { + match self { + ApiDefinitionError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ApiDefinitionError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ApiDefinitionError::Error403(error) => { + format!("LimitExceeded: {error:?}") + } + ApiDefinitionError::Error404(message) => { + format!("NotFound: {message:?}") + } + ApiDefinitionError::Error409(string) => { + format!("AlreadyExists: {string:?}") + } + ApiDefinitionError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for ApiDeploymentError { + fn map(self) -> String { + match self { + ApiDeploymentError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ApiDeploymentError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ApiDeploymentError::Error403(error) => { + format!("LimitExceeded: {error:?}") + } + ApiDeploymentError::Error404(message) => { + format!("NotFound: {message:?}") + } + ApiDeploymentError::Error409(string) => { + format!("AlreadyExists: {string:?}") + } + ApiDeploymentError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for ApiDomainError { + fn map(self) -> String { + match self { + ApiDomainError::Error400(errors) => { + format!("BadRequest: {errors:?}") + } + ApiDomainError::Error401(error) => { + format!("Unauthorized: {error:?}") + } + ApiDomainError::Error403(error) => { + format!("LimitExceeded: {error:?}") + } + ApiDomainError::Error404(message) => { + format!("NotFound: {message:?}") + } + ApiDomainError::Error409(string) => { + format!("AlreadyExists: {string:?}") + } + ApiDomainError::Error500(error) => { + format!("InternalError: {error:?}") + } + } + } +} + +impl ResponseContentErrorMapper for HealthCheckError { + fn map(self) -> String { + match self {} + } +} diff --git a/golem-cli/src/cloud/clients/gateway/health_check.rs b/golem-cli/src/cloud/clients/gateway/health_check.rs new file mode 100644 index 000000000..a2a52772b --- /dev/null +++ b/golem-cli/src/cloud/clients/gateway/health_check.rs @@ -0,0 +1,41 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::health_check::HealthCheckClient; +use async_trait::async_trait; +use golem_client::model::VersionInfo; +use tracing::debug; + +use crate::model::GolemError; + +#[derive(Clone)] +pub struct HealthCheckClientLive +{ + pub client: C, +} + +fn to_oss_version_info(v: golem_cloud_worker_client::model::VersionInfo) -> VersionInfo { + VersionInfo { version: v.version } +} + +#[async_trait] +impl HealthCheckClient + for HealthCheckClientLive +{ + async fn version(&self) -> Result { + debug!("Getting server version"); + + Ok(to_oss_version_info(self.client.version().await?)) + } +} diff --git a/golem-cli/src/cloud/clients/grant.rs b/golem-cli/src/cloud/clients/grant.rs new file mode 100644 index 000000000..cd039bb19 --- /dev/null +++ b/golem-cli/src/cloud/clients/grant.rs @@ -0,0 +1,91 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{AccountId, Role}; +use async_trait::async_trait; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait GrantClient { + async fn get_all(&self, account_id: &AccountId) -> Result, GolemError>; + async fn get(&self, account_id: &AccountId, role: Role) -> Result; + async fn put(&self, account_id: &AccountId, role: Role) -> Result<(), GolemError>; + async fn delete(&self, account_id: &AccountId, role: Role) -> Result<(), GolemError>; +} + +pub struct GrantClientLive { + pub client: C, +} + +#[async_trait] +impl GrantClient for GrantClientLive { + async fn get_all(&self, account_id: &AccountId) -> Result, GolemError> { + info!("Getting account roles."); + + let roles = self.client.get(&account_id.id).await?; + + Ok(roles.into_iter().map(api_to_cli).collect()) + } + + async fn get(&self, account_id: &AccountId, role: Role) -> Result { + info!("Getting account role."); + let role = cli_to_api(role); + + Ok(api_to_cli( + self.client.role_get(&account_id.id, &role).await?, + )) + } + + async fn put(&self, account_id: &AccountId, role: Role) -> Result<(), GolemError> { + info!("Adding account role."); + let role = cli_to_api(role); + + let _ = self.client.role_put(&account_id.id, &role).await?; + + Ok(()) + } + + async fn delete(&self, account_id: &AccountId, role: Role) -> Result<(), GolemError> { + info!("Deleting account role."); + let role = cli_to_api(role); + + let _ = self.client.role_delete(&account_id.id, &role).await?; + + Ok(()) + } +} + +fn api_to_cli(role: golem_cloud_client::model::Role) -> Role { + match role { + golem_cloud_client::model::Role::Admin {} => Role::Admin, + golem_cloud_client::model::Role::MarketingAdmin {} => Role::MarketingAdmin, + golem_cloud_client::model::Role::ViewProject {} => Role::ViewProject, + golem_cloud_client::model::Role::DeleteProject {} => Role::DeleteProject, + golem_cloud_client::model::Role::CreateProject {} => Role::CreateProject, + golem_cloud_client::model::Role::InstanceServer {} => Role::InstanceServer, + } +} + +fn cli_to_api(role: Role) -> golem_cloud_client::model::Role { + match role { + Role::Admin {} => golem_cloud_client::model::Role::Admin, + Role::MarketingAdmin {} => golem_cloud_client::model::Role::MarketingAdmin, + Role::ViewProject {} => golem_cloud_client::model::Role::ViewProject, + Role::DeleteProject {} => golem_cloud_client::model::Role::DeleteProject, + Role::CreateProject {} => golem_cloud_client::model::Role::CreateProject, + Role::InstanceServer {} => golem_cloud_client::model::Role::InstanceServer, + } +} diff --git a/golem-cli/src/cloud/clients/health_check.rs b/golem-cli/src/cloud/clients/health_check.rs new file mode 100644 index 000000000..358b578fe --- /dev/null +++ b/golem-cli/src/cloud/clients/health_check.rs @@ -0,0 +1,40 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::health_check::HealthCheckClient; +use async_trait::async_trait; +use golem_client::model::VersionInfo; +use tracing::debug; + +use crate::model::GolemError; + +#[derive(Clone)] +pub struct HealthCheckClientLive { + pub client: C, +} + +fn to_oss_version_info(v: golem_cloud_client::model::VersionInfo) -> VersionInfo { + VersionInfo { version: v.version } +} + +#[async_trait] +impl HealthCheckClient + for HealthCheckClientLive +{ + async fn version(&self) -> Result { + debug!("Getting server version"); + + Ok(to_oss_version_info(self.client.version_get().await?)) + } +} diff --git a/golem-cli/src/cloud/clients/login.rs b/golem-cli/src/cloud/clients/login.rs new file mode 100644 index 000000000..fdcb26622 --- /dev/null +++ b/golem-cli/src/cloud/clients/login.rs @@ -0,0 +1,61 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use golem_cloud_client::api::LoginClient as HttpClient; +use golem_cloud_client::model::{OAuth2Data, Token, TokenSecret, UnsafeToken}; +use golem_cloud_client::{Context, Security}; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait LoginClient { + async fn token_details(&self, manual_token: TokenSecret) -> Result; + + async fn start_oauth2(&self) -> Result; + + async fn complete_oauth2(&self, session: String) -> Result; +} + +pub struct LoginClientLive { + pub client: C, + pub context: Context, +} + +#[async_trait] +impl LoginClient for LoginClientLive { + async fn token_details(&self, manual_token: TokenSecret) -> Result { + info!("Getting token info"); + let mut context = self.context.clone(); + context.security_token = Security::Bearer(manual_token.value.to_string()); + + let client = golem_cloud_client::api::LoginClientLive { context }; + + Ok(client.v_2_login_token_get().await?) + } + + async fn start_oauth2(&self) -> Result { + info!("Start OAuth2 workflow"); + Ok(self.client.login_oauth_2_device_start_post().await?) + } + + async fn complete_oauth2(&self, session: String) -> Result { + info!("Complete OAuth2 workflow"); + Ok(self + .client + .login_oauth_2_device_complete_post(&session) + .await?) + } +} diff --git a/golem-cli/src/cloud/clients/policy.rs b/golem-cli/src/cloud/clients/policy.rs new file mode 100644 index 000000000..f49c67c9a --- /dev/null +++ b/golem-cli/src/cloud/clients/policy.rs @@ -0,0 +1,63 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use golem_cloud_client::model::{ProjectActions, ProjectPolicy, ProjectPolicyData}; +use tracing::info; + +use crate::cloud::clients::action_cli_to_api; +use crate::cloud::model::{ProjectAction, ProjectPolicyId}; +use crate::model::GolemError; + +#[async_trait] +pub trait ProjectPolicyClient { + async fn create( + &self, + name: String, + actions: Vec, + ) -> Result; + async fn get(&self, policy_id: ProjectPolicyId) -> Result; +} + +pub struct ProjectPolicyClientLive { + pub client: C, +} + +#[async_trait] +impl ProjectPolicyClient + for ProjectPolicyClientLive +{ + async fn create( + &self, + name: String, + actions: Vec, + ) -> Result { + info!("Creation project policy"); + + let actions: Vec = + actions.into_iter().map(action_cli_to_api).collect(); + let data = ProjectPolicyData { + name, + project_actions: ProjectActions { actions }, + }; + + Ok(self.client.post(&data).await?) + } + + async fn get(&self, policy_id: ProjectPolicyId) -> Result { + info!("Getting project policy"); + + Ok(self.client.project_policy_id_get(&policy_id.0).await?) + } +} diff --git a/golem-cli/src/cloud/clients/project.rs b/golem-cli/src/cloud/clients/project.rs new file mode 100644 index 000000000..436eb481b --- /dev/null +++ b/golem-cli/src/cloud/clients/project.rs @@ -0,0 +1,78 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{AccountId, ProjectId}; +use async_trait::async_trait; +use golem_cloud_client::model::{Project, ProjectDataRequest}; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait ProjectClient { + async fn create( + &self, + owner_account_id: &AccountId, + name: String, + description: Option, + ) -> Result; + async fn find(&self, name: Option) -> Result, GolemError>; + async fn find_default(&self) -> Result; + async fn delete(&self, project_id: ProjectId) -> Result<(), GolemError>; +} + +pub struct ProjectClientLive { + pub client: C, +} + +#[async_trait] +impl ProjectClient + for ProjectClientLive +{ + async fn create( + &self, + owner_account_id: &AccountId, + name: String, + description: Option, + ) -> Result { + info!("Create new project {name}."); + + let request = ProjectDataRequest { + name, + owner_account_id: owner_account_id.id.to_string(), + description: description.unwrap_or("".to_string()), + }; + Ok(self.client.post(&request).await?) + } + + async fn find(&self, name: Option) -> Result, GolemError> { + info!("Listing projects."); + + Ok(self.client.get(name.as_deref()).await?) + } + + async fn find_default(&self) -> Result { + info!("Getting default project."); + + Ok(self.client.default_get().await?) + } + + async fn delete(&self, project_id: ProjectId) -> Result<(), GolemError> { + info!("Deleting project {project_id:?}"); + + let _ = self.client.project_id_delete(&project_id.0).await?; + + Ok(()) + } +} diff --git a/golem-cli/src/cloud/clients/project_grant.rs b/golem-cli/src/cloud/clients/project_grant.rs new file mode 100644 index 000000000..82b04235c --- /dev/null +++ b/golem-cli/src/cloud/clients/project_grant.rs @@ -0,0 +1,82 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use golem_cloud_client::model::{ProjectGrant, ProjectGrantDataRequest}; +use tracing::info; + +use crate::cloud::clients::action_cli_to_api; +use crate::cloud::model::{AccountId, ProjectAction, ProjectId, ProjectPolicyId}; +use crate::model::GolemError; + +#[async_trait] +pub trait ProjectGrantClient { + async fn create( + &self, + project_id: ProjectId, + account_id: AccountId, + policy_id: ProjectPolicyId, + ) -> Result; + async fn create_actions( + &self, + project_id: ProjectId, + account_id: AccountId, + actions: Vec, + ) -> Result; +} + +pub struct ProjectGrantClientLive { + pub client: C, +} + +#[async_trait] +impl ProjectGrantClient + for ProjectGrantClientLive +{ + async fn create( + &self, + project_id: ProjectId, + account_id: AccountId, + policy_id: ProjectPolicyId, + ) -> Result { + info!("Creating project grant for policy {policy_id}."); + + let data = ProjectGrantDataRequest { + grantee_account_id: account_id.id, + project_policy_id: Some(policy_id.0), + project_actions: Vec::new(), + project_policy_name: None, + }; + + Ok(self.client.post(&project_id.0, &data).await?) + } + + async fn create_actions( + &self, + project_id: ProjectId, + account_id: AccountId, + actions: Vec, + ) -> Result { + info!("Creating project grant for actions."); + + let data = ProjectGrantDataRequest { + grantee_account_id: account_id.id, + project_policy_id: None, + project_policy_name: None, + project_actions: actions.into_iter().map(action_cli_to_api).collect(), + }; + + Ok(self.client.post(&project_id.0, &data).await?) + } +} diff --git a/golem-cli/src/cloud/clients/token.rs b/golem-cli/src/cloud/clients/token.rs new file mode 100644 index 000000000..fb0757ef0 --- /dev/null +++ b/golem-cli/src/cloud/clients/token.rs @@ -0,0 +1,71 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{AccountId, TokenId}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use golem_cloud_client::model::{CreateTokenDto, Token, UnsafeToken}; +use tracing::info; + +use crate::model::GolemError; + +#[async_trait] +pub trait TokenClient { + async fn get_all(&self, account_id: &AccountId) -> Result, GolemError>; + async fn get(&self, account_id: &AccountId, id: TokenId) -> Result; + async fn post( + &self, + account_id: &AccountId, + expires_at: DateTime, + ) -> Result; + async fn delete(&self, account_id: &AccountId, id: TokenId) -> Result<(), GolemError>; +} + +pub struct TokenClientLive { + pub client: C, +} + +#[async_trait] +impl TokenClient for TokenClientLive { + async fn get_all(&self, account_id: &AccountId) -> Result, GolemError> { + info!("Getting all tokens for used: {account_id}"); + Ok(self.client.get(&account_id.id).await?) + } + + async fn get(&self, account_id: &AccountId, id: TokenId) -> Result { + info!("Getting derails for token: {id}"); + + Ok(self.client.token_id_get(&account_id.id, &id.0).await?) + } + + async fn post( + &self, + account_id: &AccountId, + expires_at: DateTime, + ) -> Result { + info!("Creating token"); + + Ok(self + .client + .post(&account_id.id, &CreateTokenDto { expires_at }) + .await?) + } + + async fn delete(&self, account_id: &AccountId, id: TokenId) -> Result<(), GolemError> { + info!("Deleting token: {id}"); + + let _ = self.client.token_id_delete(&account_id.id, &id.0).await?; + Ok(()) + } +} diff --git a/golem-cli/src/cloud/clients/worker.rs b/golem-cli/src/cloud/clients/worker.rs new file mode 100644 index 000000000..bde66a152 --- /dev/null +++ b/golem-cli/src/cloud/clients/worker.rs @@ -0,0 +1,572 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Duration; + +use crate::clients::worker::WorkerClient; +use async_trait::async_trait; +use futures_util::{future, pin_mut, SinkExt, StreamExt}; +use golem_cloud_worker_client::model::{ + CallingConvention, FilterComparator, InvokeParameters, InvokeResult, StringFilterComparator, + WorkerAndFilter, WorkerCreatedAtFilter, WorkerCreationRequest, WorkerEnvFilter, WorkerFilter, + WorkerNameFilter, WorkerNotFilter, WorkerOrFilter, WorkerStatus, WorkerStatusFilter, + WorkerVersionFilter, WorkersMetadataRequest, +}; +use golem_cloud_worker_client::Context; +use native_tls::TlsConnector; +use serde::Deserialize; +use tokio::{task, time}; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::protocol::Message; +use tokio_tungstenite::{connect_async_tls_with_config, Connector}; +use tracing::{debug, error, info}; +use golem_client::model::ScanCursor; + +use crate::model::{ + to_oss_worker_id, ComponentId, GolemError, IdempotencyKey, WorkerMetadata, WorkerName, + WorkerUpdateMode, +}; + +#[derive(Clone)] +pub struct WorkerClientLive { + pub client: C, + pub context: Context, + pub allow_insecure: bool, +} + +fn to_cloud_invoke_parameters(ps: golem_client::model::InvokeParameters) -> InvokeParameters { + InvokeParameters { params: ps.params } +} + +fn to_oss_invoke_result(r: InvokeResult) -> golem_client::model::InvokeResult { + golem_client::model::InvokeResult { result: r.result } +} + +fn to_cloud_worker_filter(f: golem_client::model::WorkerFilter) -> WorkerFilter { + fn to_cloud_string_filter_comparator( + c: &golem_client::model::StringFilterComparator, + ) -> StringFilterComparator { + match c { + golem_client::model::StringFilterComparator::Equal => StringFilterComparator::Equal, + golem_client::model::StringFilterComparator::NotEqual => { + StringFilterComparator::NotEqual + } + golem_client::model::StringFilterComparator::Like => StringFilterComparator::Like, + golem_client::model::StringFilterComparator::NotLike => StringFilterComparator::NotLike, + } + } + + fn to_cloud_filter_name(f: golem_client::model::WorkerNameFilter) -> WorkerNameFilter { + WorkerNameFilter { + comparator: to_cloud_string_filter_comparator(&f.comparator), + value: f.value, + } + } + + fn to_cloud_filter_comparator(f: &golem_client::model::FilterComparator) -> FilterComparator { + match f { + golem_client::model::FilterComparator::Equal => FilterComparator::Equal, + golem_client::model::FilterComparator::NotEqual => FilterComparator::NotEqual, + golem_client::model::FilterComparator::GreaterEqual => FilterComparator::GreaterEqual, + golem_client::model::FilterComparator::Greater => FilterComparator::Greater, + golem_client::model::FilterComparator::LessEqual => FilterComparator::LessEqual, + golem_client::model::FilterComparator::Less => FilterComparator::Less, + } + } + + fn to_cloud_worker_status(s: &golem_client::model::WorkerStatus) -> WorkerStatus { + match s { + golem_client::model::WorkerStatus::Running => WorkerStatus::Running, + golem_client::model::WorkerStatus::Idle => WorkerStatus::Idle, + golem_client::model::WorkerStatus::Suspended => WorkerStatus::Suspended, + golem_client::model::WorkerStatus::Interrupted => WorkerStatus::Interrupted, + golem_client::model::WorkerStatus::Retrying => WorkerStatus::Retrying, + golem_client::model::WorkerStatus::Failed => WorkerStatus::Failed, + golem_client::model::WorkerStatus::Exited => WorkerStatus::Exited, + } + } + + fn to_cloud_filter_status(f: golem_client::model::WorkerStatusFilter) -> WorkerStatusFilter { + WorkerStatusFilter { + comparator: to_cloud_filter_comparator(&f.comparator), + value: to_cloud_worker_status(&f.value), + } + } + fn to_cloud_filter_version(f: golem_client::model::WorkerVersionFilter) -> WorkerVersionFilter { + WorkerVersionFilter { + comparator: to_cloud_filter_comparator(&f.comparator), + value: f.value, + } + } + fn to_cloud_filter_created_at( + f: golem_client::model::WorkerCreatedAtFilter, + ) -> WorkerCreatedAtFilter { + WorkerCreatedAtFilter { + comparator: to_cloud_filter_comparator(&f.comparator), + value: f.value, + } + } + fn to_cloud_filter_env(f: golem_client::model::WorkerEnvFilter) -> WorkerEnvFilter { + let golem_client::model::WorkerEnvFilter { + name, + comparator, + value, + } = f; + + WorkerEnvFilter { + name, + comparator: to_cloud_string_filter_comparator(&comparator), + value, + } + } + fn to_cloud_filter_and(f: golem_client::model::WorkerAndFilter) -> WorkerAndFilter { + WorkerAndFilter { + filters: f.filters.into_iter().map(to_cloud_worker_filter).collect(), + } + } + fn to_cloud_filter_or(f: golem_client::model::WorkerOrFilter) -> WorkerOrFilter { + WorkerOrFilter { + filters: f.filters.into_iter().map(to_cloud_worker_filter).collect(), + } + } + fn to_cloud_filter_not(f: golem_client::model::WorkerNotFilter) -> WorkerNotFilter { + WorkerNotFilter { + filter: to_cloud_worker_filter(f.filter), + } + } + + match f { + golem_client::model::WorkerFilter::Name(f) => WorkerFilter::Name(to_cloud_filter_name(f)), + golem_client::model::WorkerFilter::Status(f) => { + WorkerFilter::Status(to_cloud_filter_status(f)) + } + golem_client::model::WorkerFilter::Version(f) => { + WorkerFilter::Version(to_cloud_filter_version(f)) + } + golem_client::model::WorkerFilter::CreatedAt(f) => { + WorkerFilter::CreatedAt(to_cloud_filter_created_at(f)) + } + golem_client::model::WorkerFilter::Env(f) => WorkerFilter::Env(to_cloud_filter_env(f)), + golem_client::model::WorkerFilter::And(f) => WorkerFilter::And(to_cloud_filter_and(f)), + golem_client::model::WorkerFilter::Or(f) => WorkerFilter::Or(to_cloud_filter_or(f)), + golem_client::model::WorkerFilter::Not(f) => { + WorkerFilter::Not(Box::new(to_cloud_filter_not(*f))) + } + } +} + +#[async_trait] +impl WorkerClient + for WorkerClientLive +{ + async fn new_worker( + &self, + name: WorkerName, + component_id: ComponentId, + args: Vec, + env: Vec<(String, String)>, + ) -> Result { + info!("Creating worker {name} of {}", component_id.0); + + Ok(to_oss_worker_id( + self.client + .launch_new_worker( + &component_id.0, + &WorkerCreationRequest { + name: name.0, + args, + env: env.into_iter().collect(), + }, + ) + .await? + .worker_id, + )) + } + + async fn invoke_and_await( + &self, + name: WorkerName, + component_id: ComponentId, + function: String, + parameters: golem_client::model::InvokeParameters, + idempotency_key: Option, + use_stdio: bool, + ) -> Result { + info!( + "Invoke and await for function {function} in {}/{}", + component_id.0, name.0 + ); + + let calling_convention = if use_stdio { + CallingConvention::Stdio + } else { + CallingConvention::Component + }; + + Ok(to_oss_invoke_result( + self.client + .invoke_and_await_function( + &component_id.0, + &name.0, + idempotency_key.as_ref().map(|k| k.0.as_str()), + &function, + Some(&calling_convention), + &to_cloud_invoke_parameters(parameters), + ) + .await?, + )) + } + + async fn invoke( + &self, + name: WorkerName, + component_id: ComponentId, + function: String, + parameters: golem_client::model::InvokeParameters, + idempotency_key: Option, + ) -> Result<(), GolemError> { + info!( + "Invoke function {function} in {}/{}", + component_id.0, name.0 + ); + + let _ = self + .client + .invoke_function( + &component_id.0, + &name.0, + idempotency_key.as_ref().map(|k| k.0.as_str()), + &function, + &to_cloud_invoke_parameters(parameters), + ) + .await?; + Ok(()) + } + + async fn interrupt( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result<(), GolemError> { + info!("Interrupting {}/{}", component_id.0, name.0); + + let _ = self + .client + .interrupt_worker(&component_id.0, &name.0, Some(false)) + .await?; + Ok(()) + } + + async fn simulated_crash( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result<(), GolemError> { + info!("Simulating crash of {}/{}", component_id.0, name.0); + + let _ = self + .client + .interrupt_worker(&component_id.0, &name.0, Some(true)) + .await?; + Ok(()) + } + + async fn delete(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { + info!("Deleting worker {}/{}", component_id.0, name.0); + + let _ = self.client.delete_worker(&component_id.0, &name.0).await?; + Ok(()) + } + + async fn get_metadata( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result { + info!("Getting worker {}/{} metadata", component_id.0, name.0); + + Ok(self + .client + .get_worker_metadata(&component_id.0, &name.0) + .await? + .into()) + } + + async fn find_metadata( + &self, + component_id: ComponentId, + filter: Option, + cursor: Option, + count: Option, + precise: Option, + ) -> Result { + info!( + "Getting workers metadata for component: {}, filter: {}", + component_id.0, + filter.is_some() + ); + + Ok(self + .client + .find_workers_metadata( + &component_id.0, + &WorkersMetadataRequest { + filter: filter.map(to_cloud_worker_filter), + cursor: cursor.map(|c| c.cursor), // TODO: unify cloud and OSS + count, + precise, + }, + ) + .await? + .into()) + } + + async fn list_metadata( + &self, + component_id: ComponentId, + filter: Option>, + cursor: Option, + count: Option, + precise: Option, + ) -> Result { + info!( + "Getting workers metadata for component: {}, filter: {}", + component_id.0, + filter + .clone() + .map(|fs| fs.join(" AND ")) + .unwrap_or("N/A".to_string()) + ); + + let filter: Option<&[String]> = filter.as_deref(); + + Ok(self + .client + .get_workers_metadata(&component_id.0, filter, cursor.map(|c| c.cursor), count, precise) // TODO: unify cloud and OSS + .await? + .into()) + } + + async fn connect(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { + let mut url = self.context.base_url.clone(); + + let ws_schema = if url.scheme() == "http" { "ws" } else { "wss" }; + + url.set_scheme(ws_schema) + .map_err(|_| GolemError("Can't set schema.".to_string()))?; + + url.path_segments_mut() + .map_err(|_| GolemError("Can't get path.".to_string()))? + .push("v2") + .push("components") + .push(&component_id.0.to_string()) + .push("workers") + .push(&name.0) + .push("connect"); + + let mut request = url + .into_client_request() + .map_err(|e| GolemError(format!("Can't create request: {e}")))?; + let headers = request.headers_mut(); + + if let Some(token) = self.context.bearer_token() { + headers.insert( + "Authorization", + format!("Bearer {}", token).parse().unwrap(), + ); + } + + let connector = if self.allow_insecure { + Some(Connector::NativeTls( + TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(), + )) + } else { + None + }; + + let (ws_stream, _) = connect_async_tls_with_config(request, None, false, connector) + .await + .map_err(|e| match e { + tungstenite::error::Error::Http(http_error_response) => { + match http_error_response.body().clone() { + Some(body) => GolemError(format!( + "Failed Websocket. Http error: {}, {}", + http_error_response.status(), + String::from_utf8_lossy(&body) + )), + None => GolemError(format!( + "Failed Websocket. Http error: {}", + http_error_response.status() + )), + } + } + _ => GolemError(format!("Failed Websocket. Error: {}", e)), + })?; + + let (mut write, read) = ws_stream.split(); + + let pings = task::spawn(async move { + let mut interval = time::interval(Duration::from_secs(5)); // TODO configure + + let mut cnt: i32 = 1; + + loop { + interval.tick().await; + + write + .send(Message::Ping(cnt.to_ne_bytes().to_vec())) + .await + .unwrap(); // TODO: handle errors: map_err(|e| GolemError(format!("Ping failure: {e}")))?; + + cnt += 1; + } + }); + + let read_res = read.for_each(|message_or_error| async { + match message_or_error { + Err(error) => { + debug!("Error reading message: {}", error); + } + Ok(message) => { + let instance_connect_msg = match message { + Message::Text(str) => { + let parsed: serde_json::Result = + serde_json::from_str(&str); + Some(parsed.unwrap()) // TODO: error handling + } + Message::Binary(data) => { + let parsed: serde_json::Result = + serde_json::from_slice(&data); + Some(parsed.unwrap()) // TODO: error handling + } + Message::Ping(_) => { + debug!("Ping received from server"); + None + } + Message::Pong(_) => { + debug!("Pong received from server"); + None + } + Message::Close(details) => { + match details { + Some(closed_frame) => { + error!("Connection Closed: {}", closed_frame); + } + None => { + info!("Connection Closed"); + } + } + None + } + Message::Frame(_) => { + info!("Ignore unexpected frame"); + None + } + }; + + match instance_connect_msg { + None => {} + Some(msg) => match msg.event { + WorkerEvent::Stdout(StdOutLog { message }) => { + print!("{message}") + } + WorkerEvent::Stderr(StdErrLog { message }) => { + print!("{message}") + } + WorkerEvent::Log(Log { + level, + context, + message, + }) => match level { + 0 => tracing::trace!(message, context = context), + 1 => tracing::debug!(message, context = context), + 2 => tracing::info!(message, context = context), + 3 => tracing::warn!(message, context = context), + _ => tracing::error!(message, context = context), + }, + }, + } + } + } + }); + + pin_mut!(read_res, pings); + + future::select(pings, read_res).await; + + Ok(()) + } + + async fn update( + &self, + name: WorkerName, + component_id: ComponentId, + mode: WorkerUpdateMode, + target_version: u64, + ) -> Result<(), GolemError> { + info!("Updating worker {name} of {}", component_id.0); + let update_mode = match mode { + WorkerUpdateMode::Automatic => { + golem_cloud_worker_client::model::WorkerUpdateMode::Automatic + } + WorkerUpdateMode::Manual => golem_cloud_worker_client::model::WorkerUpdateMode::Manual, + }; + + let _ = self + .client + .update_worker( + &component_id.0, + &name.0, + &golem_cloud_worker_client::model::UpdateWorkerRequest { + mode: update_mode, + target_version, + }, + ) + .await?; + Ok(()) + } +} + +#[derive(Deserialize, Debug)] +struct InstanceConnectMessage { + pub event: WorkerEvent, +} + +#[derive(Deserialize, Debug)] +enum WorkerEvent { + Stdout(StdOutLog), + Stderr(StdErrLog), + Log(Log), +} + +#[derive(Deserialize, Debug)] +struct StdOutLog { + message: String, +} + +#[derive(Deserialize, Debug)] +struct StdErrLog { + message: String, +} + +#[derive(Deserialize, Debug)] +struct Log { + pub level: i32, + pub context: String, + pub message: String, +} diff --git a/golem-cli/src/cloud/command.rs b/golem-cli/src/cloud/command.rs new file mode 100644 index 000000000..471ec1496 --- /dev/null +++ b/golem-cli/src/cloud/command.rs @@ -0,0 +1,200 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::command::account::AccountSubcommand; +use crate::cloud::command::api_definition::ApiDefinitionSubcommand; +use crate::cloud::command::api_deployment::ApiDeploymentSubcommand; +use crate::cloud::command::certificate::CertificateSubcommand; +use crate::cloud::command::domain::DomainSubcommand; +use crate::cloud::command::policy::ProjectPolicySubcommand; +use crate::cloud::command::project::ProjectSubcommand; +use crate::cloud::command::token::TokenSubcommand; +use crate::cloud::command::worker::WorkerSubcommand; +use crate::cloud::model::{AccountId, ProjectAction, ProjectPolicyId, ProjectRef}; +use crate::model::Format; +use clap::{Parser, Subcommand}; +use clap_verbosity_flag::Verbosity; +use component::ComponentSubCommand; +use golem_examples::model::{ExampleName, GuestLanguage, GuestLanguageTier, PackageName}; +use std::path::PathBuf; +use uuid::Uuid; + +pub mod account; +pub mod api_definition; +pub mod api_deployment; +pub mod certificate; +pub mod component; +pub mod domain; +pub mod policy; +pub mod project; +pub mod token; +pub mod worker; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum CloudCommand { + /// Upload and manage Golem components + #[command()] + Component { + #[command(subcommand)] + subcommand: ComponentSubCommand, + }, + + /// Manage Golem workers + #[command()] + Worker { + #[command(subcommand)] + subcommand: WorkerSubcommand, + }, + + /// Manage accounts + #[command()] + Account { + /// The account ID to operate on + #[arg(short = 'A', long)] + account_id: Option, + + #[command(subcommand)] + subcommand: AccountSubcommand, + }, + + /// Manage access tokens + #[command()] + Token { + /// The account ID to operate on + #[arg(short = 'A', long)] + account_id: Option, + + #[command(subcommand)] + subcommand: TokenSubcommand, + }, + + /// Manage projects + #[command()] + Project { + #[command(subcommand)] + subcommand: ProjectSubcommand, + }, + + /// Share a project with another account + #[command()] + Share { + /// Project to be shared + #[command(flatten)] + project_ref: ProjectRef, + + /// User account the project will be shared with + #[arg(long)] + recipient_account_id: AccountId, + + /// The sharing policy's identifier. If not provided, use `--project-actions` instead + #[arg(long, required = true, conflicts_with = "project_actions")] + project_policy_id: Option, + + /// A list of actions to be granted to the recipient account. If not provided, use `--project-policy-id` instead + #[arg( + short = 'A', + long, + required = true, + conflicts_with = "project_policy_id" + )] + project_actions: Option>, + }, + + /// Manage project sharing policies + #[command()] + ProjectPolicy { + #[command(subcommand)] + subcommand: ProjectPolicySubcommand, + }, + + /// Create a new Golem component from built-in examples + #[command()] + New { + /// Name of the example to use + #[arg(short, long)] + example: ExampleName, + + /// The new component's name + #[arg(short, long)] + component_name: golem_examples::model::ComponentName, + + /// The package name of the generated component (in namespace:name format) + #[arg(short, long)] + package_name: Option, + }, + /// Lists the built-in examples available for creating new components + #[command()] + ListExamples { + /// The minimum language tier to include in the list + #[arg(short, long)] + min_tier: Option, + + /// Filter examples by a given guest language + #[arg(short, long)] + language: Option, + }, + + /// WASM RPC stub generator + #[cfg(feature = "stubgen")] + Stubgen { + #[command(subcommand)] + subcommand: golem_wasm_rpc_stubgen::Command, + }, + + /// Manage Golem api definitions + #[command()] + ApiDefinition { + #[command(subcommand)] + subcommand: ApiDefinitionSubcommand, + }, + + /// Manage Golem api deployments + #[command()] + ApiDeployment { + #[command(subcommand)] + subcommand: ApiDeploymentSubcommand, + }, + + #[command()] + Certificate { + #[command(subcommand)] + subcommand: CertificateSubcommand, + }, + + #[command()] + Domain { + #[command(subcommand)] + subcommand: DomainSubcommand, + }, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None, rename_all = "kebab-case")] +pub struct GolemCloudCommand { + #[arg(short = 'D', long, value_name = "DIR", value_hint = clap::ValueHint::DirPath)] + pub config_directory: Option, + + #[arg(short = 'T', long)] + pub auth_token: Option, + + #[command(flatten)] + pub verbosity: Verbosity, + + #[arg(short = 'F', long, default_value = "text")] + pub format: Format, + + #[command(subcommand)] + pub command: CloudCommand, +} diff --git a/golem-cli/src/cloud/command/account.rs b/golem-cli/src/cloud/command/account.rs new file mode 100644 index 000000000..831030741 --- /dev/null +++ b/golem-cli/src/cloud/command/account.rs @@ -0,0 +1,117 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Subcommand; + +use crate::cloud::model::{AccountId, Role}; +use crate::cloud::service::account::AccountService; +use crate::cloud::service::grant::GrantService; +use crate::model::{GolemError, GolemResult}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum AccountSubcommand { + /// Get information about the account + #[command()] + Get {}, + + /// Update some information about the account + #[command()] + Update { + /// Set the account's name + // TODO: validate non-empty + #[arg(short = 'n', long)] + account_name: Option, + + /// Set the account's email address + #[arg(short = 'e', long)] + account_email: Option, + }, + + /// Add a new account + #[command()] + Add { + /// The new account's name + #[arg(short = 'n', long)] + account_name: String, + + /// The new account's email address + #[arg(short = 'e', long)] + account_email: String, + }, + + /// Delete the account + #[command()] + Delete {}, + + /// Manage the account's roles + #[command()] + Grant { + #[command(subcommand)] + subcommand: GrantSubcommand, + }, +} + +#[derive(Subcommand, Debug)] +#[command()] +pub enum GrantSubcommand { + /// Get the roles granted to the account + #[command()] + Get {}, + + /// Grant a new role to the account + #[command()] + Add { + #[arg(value_name = "ROLE")] + role: Role, + }, + + /// Remove a role from the account + #[command()] + Delete { + #[arg(value_name = "ROLE")] + role: Role, + }, +} + +impl AccountSubcommand { + pub async fn handle( + self, + account_id: Option, + service: &(dyn AccountService + Send + Sync), + grant: &(dyn GrantService + Send + Sync), + ) -> Result { + match self { + AccountSubcommand::Get {} => service.get(account_id).await, + AccountSubcommand::Update { + account_name, + account_email, + } => { + service + .update(account_name, account_email, account_id) + .await + } + AccountSubcommand::Add { + account_name, + account_email, + } => service.add(account_name, account_email).await, + AccountSubcommand::Delete {} => service.delete(account_id).await, + AccountSubcommand::Grant { subcommand } => match subcommand { + GrantSubcommand::Get {} => grant.get(account_id).await, + GrantSubcommand::Add { role } => grant.add(role, account_id).await, + GrantSubcommand::Delete { role } => grant.delete(role, account_id).await, + }, + } + } +} diff --git a/golem-cli/src/cloud/command/api_definition.rs b/golem-cli/src/cloud/command/api_definition.rs new file mode 100644 index 000000000..b9d6d5a6d --- /dev/null +++ b/golem-cli/src/cloud/command/api_definition.rs @@ -0,0 +1,163 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{ProjectId, ProjectRef}; +use crate::cloud::service::project::ProjectService; +use crate::model::{ + ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult, PathBufOrStdin, +}; +use crate::service::api_definition::ApiDefinitionService; +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ApiDefinitionSubcommand { + /// Lists all api definitions + #[command()] + List { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Api definition id to get all versions. Optional. + #[arg(short, long)] + id: Option, + }, + + /// Creates an api definition + /// + /// Golem API definition file format expected + #[command()] + Add { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// The Golem API definition file + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Updates an api definition + /// + /// Golem API definition file format expected + #[command()] + Update { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// The Golem API definition file + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Import OpenAPI file as api definition + #[command()] + Import { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// The OpenAPI json or yaml file to be used as the api definition + /// + /// Json format expected unless file name ends up in `.yaml` + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Retrieves metadata about an existing api definition + #[command()] + Get { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + + /// Version of the api definition + #[arg(short = 'V', long)] + version: ApiDefinitionVersion, + }, + + /// Deletes an existing api definition + #[command()] + Delete { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + + /// Version of the api definition + #[arg(short = 'V', long)] + version: ApiDefinitionVersion, + }, +} + +impl ApiDefinitionSubcommand { + pub async fn handle( + self, + service: &(dyn ApiDefinitionService + Send + Sync), + projects: &(dyn ProjectService + Send + Sync), + ) -> Result { + match self { + ApiDefinitionSubcommand::Get { + project_ref, + id, + version, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.get(id, version, &project_id).await + } + ApiDefinitionSubcommand::Add { + project_ref, + definition, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.add(definition, &project_id).await + } + ApiDefinitionSubcommand::Update { + project_ref, + definition, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.update(definition, &project_id).await + } + ApiDefinitionSubcommand::Import { + project_ref, + definition, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.import(definition, &project_id).await + } + ApiDefinitionSubcommand::List { project_ref, id } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.list(id, &project_id).await + } + ApiDefinitionSubcommand::Delete { + project_ref, + id, + version, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.delete(id, version, &project_id).await + } + } + } +} diff --git a/golem-cli/src/cloud/command/api_deployment.rs b/golem-cli/src/cloud/command/api_deployment.rs new file mode 100644 index 000000000..b3b05c107 --- /dev/null +++ b/golem-cli/src/cloud/command/api_deployment.rs @@ -0,0 +1,102 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{ProjectId, ProjectRef}; +use crate::cloud::service::project::ProjectService; +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult}; +use crate::service::api_deployment::ApiDeploymentService; +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ApiDeploymentSubcommand { + /// Create or update deployment + #[command()] + Deploy { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + + /// Api definition version + #[arg(short = 'V', long)] + version: ApiDefinitionVersion, + + #[arg(short = 'H', long)] + host: String, + + #[arg(short, long)] + subdomain: String, // TODO: unify cloud with OSS + }, + + /// Get api deployment + #[command()] + Get { + /// Deployment site + #[arg(value_name = "subdomain.host")] + site: String, + }, + + /// List api deployment for api definition + #[command()] + List { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + }, + + /// Delete api deployment + #[command()] + Delete { + /// Deployment site + #[arg(value_name = "subdomain.host")] + site: String, + }, +} + +impl ApiDeploymentSubcommand { + pub async fn handle( + self, + service: &(dyn ApiDeploymentService + Send + Sync), + projects: &(dyn ProjectService + Send + Sync), + ) -> Result { + match self { + ApiDeploymentSubcommand::Deploy { + project_ref, + id, + version, + host, + subdomain, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service + .deploy(id, version, host, Some(subdomain), &project_id) + .await + } + ApiDeploymentSubcommand::Get { site } => service.get(site).await, + ApiDeploymentSubcommand::List { project_ref, id } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.list(id, &project_id).await + } + ApiDeploymentSubcommand::Delete { site } => service.delete(site).await, + } + } +} diff --git a/golem-cli/src/cloud/command/certificate.rs b/golem-cli/src/cloud/command/certificate.rs new file mode 100644 index 000000000..42ba9d1b2 --- /dev/null +++ b/golem-cli/src/cloud/command/certificate.rs @@ -0,0 +1,86 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::ProjectRef; +use crate::cloud::service::certificate::CertificateService; +use clap::Subcommand; +use uuid::Uuid; + +use crate::model::{GolemError, GolemResult, PathBufOrStdin}; + +#[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: Uuid, + }, +} + +impl CertificateSubcommand { + pub async fn handle( + self, + service: &(dyn CertificateService + Send + Sync), + ) -> Result { + match self { + CertificateSubcommand::Get { + project_ref, + certificate_id, + } => service.get(project_ref, certificate_id).await, + CertificateSubcommand::Add { + project_ref, + domain_name, + certificate_body, + certificate_private_key, + } => { + service + .add( + project_ref, + domain_name, + certificate_body, + certificate_private_key, + ) + .await + } + CertificateSubcommand::Delete { + project_ref, + certificate_id, + } => service.delete(project_ref, certificate_id).await, + } + } +} diff --git a/golem-cli/src/cloud/command/component.rs b/golem-cli/src/cloud/command/component.rs new file mode 100644 index 000000000..5523e051a --- /dev/null +++ b/golem-cli/src/cloud/command/component.rs @@ -0,0 +1,120 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{CloudComponentIdOrName, ProjectId, ProjectRef}; +use crate::cloud::service::project::ProjectService; +use crate::model::{ComponentName, GolemError, GolemResult, PathBufOrStdin}; +use crate::service::component::ComponentService; +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ComponentSubCommand { + /// Creates a new component with a given name by uploading the component WASM + #[command()] + Add { + /// The newly created component's owner project + #[command(flatten)] + project_ref: ProjectRef, + + /// Name of the newly created component + #[arg(short, long)] + component_name: ComponentName, + + /// The WASM file to be used as a Golem component + #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] + component_file: PathBufOrStdin, // TODO: validate exists + }, + + /// Updates an existing component by uploading a new version of its WASM + #[command()] + Update { + /// The component name or identifier to update + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// The WASM file to be used as a new version of the Golem component + #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] + component_file: PathBufOrStdin, // TODO: validate exists + }, + + /// Lists the existing components + #[command()] + List { + /// The project to list components from + #[command(flatten)] + project_ref: ProjectRef, + + /// Optionally look for only components matching a given name + #[arg(short, long)] + component_name: Option, + }, + /// Get component + #[command()] + Get { + /// The Golem component id or name + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// The version of the component + #[arg(short = 't', long)] + version: Option, + }, +} + +impl ComponentSubCommand { + pub async fn handle( + self, + service: &(dyn ComponentService + Send + Sync), + projects: &(dyn ProjectService + Send + Sync), + ) -> Result { + match self { + ComponentSubCommand::Add { + project_ref, + component_name, + component_file, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service + .add(component_name, component_file, Some(project_id)) + .await + } + ComponentSubCommand::Update { + component_id_or_name, + component_file, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .update(component_id_or_name, component_file, project_id) + .await + } + ComponentSubCommand::List { + project_ref, + component_name, + } => { + let project_id = projects.resolve_id_or_default(project_ref).await?; + service.list(component_name, Some(project_id)).await + } + ComponentSubCommand::Get { + component_id_or_name, + version, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service.get(component_id_or_name, version, project_id).await + } + } + } +} diff --git a/golem-cli/src/cloud/command/domain.rs b/golem-cli/src/cloud/command/domain.rs new file mode 100644 index 000000000..e562d31c3 --- /dev/null +++ b/golem-cli/src/cloud/command/domain.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::ProjectRef; +use crate::cloud::service::domain::DomainService; +use clap::Subcommand; + +use crate::model::{GolemError, GolemResult}; + +#[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, + }, +} + +impl DomainSubcommand { + pub async fn handle( + self, + service: &(dyn DomainService + Send + Sync), + ) -> Result { + match self { + DomainSubcommand::Get { project_ref } => service.get(project_ref).await, + DomainSubcommand::Add { + project_ref, + domain_name, + } => service.add(project_ref, domain_name).await, + DomainSubcommand::Delete { + project_ref, + domain_name, + } => service.delete(project_ref, domain_name).await, + } + } +} diff --git a/golem-cli/src/cloud/command/policy.rs b/golem-cli/src/cloud/command/policy.rs new file mode 100644 index 000000000..d4c0e8f3d --- /dev/null +++ b/golem-cli/src/cloud/command/policy.rs @@ -0,0 +1,59 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Subcommand; + +use crate::cloud::model::{ProjectAction, ProjectPolicyId}; +use crate::cloud::service::policy::ProjectPolicyService; +use crate::model::{GolemError, GolemResult}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ProjectPolicySubcommand { + /// Creates a new project sharing policy + #[command()] + Add { + /// Name of the policy + #[arg(long)] + project_policy_name: String, + + /// List of actions allowed by the policy + #[arg(value_name = "Actions")] + project_actions: Vec, + }, + + /// Gets the existing project sharing policies + #[command()] + Get { + #[arg(value_name = "ID")] + project_policy_id: ProjectPolicyId, + }, +} + +impl ProjectPolicySubcommand { + pub async fn handle( + self, + service: &(dyn ProjectPolicyService + Send + Sync), + ) -> Result { + match self { + ProjectPolicySubcommand::Add { + project_actions, + project_policy_name, + } => service.add(project_policy_name, project_actions).await, + ProjectPolicySubcommand::Get { project_policy_id } => { + service.get(project_policy_id).await + } + } + } +} diff --git a/golem-cli/src/cloud/command/project.rs b/golem-cli/src/cloud/command/project.rs new file mode 100644 index 000000000..9686cfcc7 --- /dev/null +++ b/golem-cli/src/cloud/command/project.rs @@ -0,0 +1,62 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Subcommand; + +use crate::cloud::service::project::ProjectService; +use crate::model::{GolemError, GolemResult}; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ProjectSubcommand { + /// Add a new project + #[command()] + Add { + /// The new project's name + #[arg(short, long)] + project_name: String, + + /// The new project's description + #[arg(short = 't', long)] + project_description: Option, + }, + + /// Lists existing projects + #[command()] + List { + /// Optionally filter projects by name + #[arg(short, long)] + project_name: Option, + }, + + /// Gets the default project which is used when no explicit project is specified + #[command()] + GetDefault {}, +} + +impl ProjectSubcommand { + pub async fn handle( + self, + service: &(dyn ProjectService + Send + Sync), + ) -> Result { + match self { + ProjectSubcommand::Add { + project_name, + project_description, + } => service.add(project_name, project_description).await, + ProjectSubcommand::List { project_name } => service.list(project_name).await, + ProjectSubcommand::GetDefault {} => service.get_default().await, + } + } +} diff --git a/golem-cli/src/cloud/command/token.rs b/golem-cli/src/cloud/command/token.rs new file mode 100644 index 000000000..434a4456a --- /dev/null +++ b/golem-cli/src/cloud/command/token.rs @@ -0,0 +1,66 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::{DateTime, Utc}; +use clap::Subcommand; + +use crate::cloud::model::{AccountId, TokenId}; +use crate::cloud::service::token::TokenService; +use crate::model::{GolemError, GolemResult}; + +fn parse_instant( + s: &str, +) -> Result, Box> { + match s.parse::>() { + Ok(dt) => Ok(dt), + Err(err) => Err(err.into()), + } +} + +#[derive(Subcommand, Debug)] +#[command()] +pub enum TokenSubcommand { + /// List the existing tokens + #[command()] + List {}, + + /// Add a new token + #[command()] + Add { + /// Expiration date of the generated token + #[arg(long, value_parser = parse_instant, default_value = "2100-01-01T00:00:00Z")] + expires_at: DateTime, + }, + + /// Delete an existing token + #[command()] + Delete { + #[arg(value_name = "TOKEN")] + token_id: TokenId, + }, +} + +impl TokenSubcommand { + pub async fn handle( + self, + account_id: Option, + service: &(dyn TokenService + Send + Sync), + ) -> Result { + match self { + TokenSubcommand::List {} => service.list(account_id).await, + TokenSubcommand::Add { expires_at } => service.add(expires_at, account_id).await, + TokenSubcommand::Delete { token_id } => service.delete(token_id, account_id).await, + } + } +} diff --git a/golem-cli/src/cloud/command/worker.rs b/golem-cli/src/cloud/command/worker.rs new file mode 100644 index 000000000..b4d632b86 --- /dev/null +++ b/golem-cli/src/cloud/command/worker.rs @@ -0,0 +1,396 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::{CloudComponentIdOrName, ProjectId}; +use crate::cloud::service::project::ProjectService; +use clap::builder::ValueParser; +use clap::Subcommand; +use golem_client::model::ScanCursor; + +use crate::model::{ + Format, GolemError, GolemResult, IdempotencyKey, JsonValueParser, WorkerName, WorkerUpdateMode, +}; +use crate::parse_key_val; +use crate::service::worker::WorkerService; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum WorkerSubcommand { + /// Creates a new idle worker + #[command()] + Add { + /// The Golem component to use for the worker, identified by either its name or its component ID + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the newly created worker + #[arg(short, long)] + worker_name: WorkerName, + + /// List of environment variables (key-value pairs) passed to the worker + #[arg(short, long, value_parser = parse_key_val, value_name = "ENV=VAL")] + env: Vec<(String, String)>, + + /// List of command line arguments passed to the worker + #[arg(value_name = "args")] + args: Vec, + }, + + /// Generates an idempotency key for achieving at-most-one invocation when doing retries + #[command()] + IdempotencyKey {}, + + /// Invokes a worker and waits for its completion + #[command()] + InvokeAndAwait { + /// The Golem component the worker to be invoked belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + + /// A pre-generated idempotency key + #[arg(short = 'k', long)] + idempotency_key: Option, + + /// Name of the function to be invoked + #[arg(short, long)] + function: String, + + /// JSON array representing the parameters to be passed to the function + #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] + parameters: Option, + + /// Function parameter in WAVE format + /// + /// You can specify this argument multiple times for multiple parameters. + #[arg( + short = 'p', + long = "param", + value_name = "wave", + conflicts_with = "parameters" + )] + wave: Vec, + + /// Enables the STDIO cal;ing convention, passing the parameters through stdin instead of a typed exported interface + #[arg(short = 's', long, default_value_t = false)] + use_stdio: bool, + }, + + /// Triggers a function invocation on a worker without waiting for its completion + #[command()] + Invoke { + /// The Golem component the worker to be invoked belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + + /// A pre-generated idempotency key + #[arg(short = 'k', long)] + idempotency_key: Option, + + /// Name of the function to be invoked + #[arg(short, long)] + function: String, + + /// JSON array representing the parameters to be passed to the function + #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] + parameters: Option, + + /// Function parameter in WAVE format + /// + /// You can specify this argument multiple times for multiple parameters. + #[arg( + short = 'p', + long = "param", + value_name = "wave", + conflicts_with = "parameters" + )] + wave: Vec, + }, + + /// Connect to a worker and live stream its standard output, error and log channels + #[command()] + Connect { + /// The Golem component the worker to be connected to belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Interrupts a running worker + #[command()] + Interrupt { + /// The Golem component the worker to be interrupted belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Simulates a crash on a worker for testing purposes. + /// + /// The worker starts recovering and resuming immediately. + #[command()] + SimulatedCrash { + /// The Golem component the worker to be crashed belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Deletes a worker + #[command()] + Delete { + /// The Golem component the worker to be deleted belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Retrieves metadata about an existing worker + #[command()] + Get { + /// The Golem component the worker to be retrieved belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + /// Retrieves metadata about an existing workers in a component + #[command()] + List { + /// The Golem component the workers to be retrieved belongs to + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Filter for worker metadata in form of `property op value`. + /// + /// Filter examples: `name = worker-name`, `version >= 0`, `status = Running`, `env.var1 = value`. + /// Can be used multiple times (AND condition is applied between them) + #[arg(short, long)] + filter: Option>, + + /// Position where to start listing, if not provided, starts from the beginning + /// + /// It is used to get the next page of results. To get next page, use the cursor returned in the response + #[arg(short = 'P', long)] + cursor: Option, + + /// Count of listed values, if count is not provided, returns all values + #[arg(short = 'n', long)] + count: Option, + + /// Precision in relation to worker status, if true, calculate the most up-to-date status for each worker, default is false + #[arg(short, long)] + precise: Option, + }, + /// Updates a worker + #[command()] + Update { + /// The Golem component of the worker, identified by either its name or its component ID + #[command(flatten)] + component_id_or_name: CloudComponentIdOrName, + + /// Name of the worker to update + #[arg(short, long)] + worker_name: WorkerName, + + /// Update mode - auto or manual + #[arg(short, long)] + mode: WorkerUpdateMode, + + /// The new version of the updated worker + #[arg(short = 't', long)] + target_version: u64, + }, +} + +impl WorkerSubcommand { + pub async fn handle( + self, + format: Format, + service: &(dyn WorkerService + Send + Sync), + projects: &(dyn ProjectService + Send + Sync), + ) -> Result { + match self { + WorkerSubcommand::Add { + component_id_or_name, + worker_name, + env, + args, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .add(component_id_or_name, worker_name, env, args, project_id) + .await + } + WorkerSubcommand::IdempotencyKey {} => service.idempotency_key().await, + WorkerSubcommand::InvokeAndAwait { + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + use_stdio, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .invoke_and_await( + format, + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + use_stdio, + project_id, + ) + .await + } + WorkerSubcommand::Invoke { + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .invoke( + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + project_id, + ) + .await + } + WorkerSubcommand::Connect { + component_id_or_name, + worker_name, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .connect(component_id_or_name, worker_name, project_id) + .await + } + WorkerSubcommand::Interrupt { + component_id_or_name, + worker_name, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .interrupt(component_id_or_name, worker_name, project_id) + .await + } + WorkerSubcommand::SimulatedCrash { + component_id_or_name, + worker_name, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .simulated_crash(component_id_or_name, worker_name, project_id) + .await + } + WorkerSubcommand::Delete { + component_id_or_name, + worker_name, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .delete(component_id_or_name, worker_name, project_id) + .await + } + WorkerSubcommand::Get { + component_id_or_name, + worker_name, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .get(component_id_or_name, worker_name, project_id) + .await + } + WorkerSubcommand::List { + component_id_or_name, + filter, + count, + cursor, + precise, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .list( + component_id_or_name, + filter, + count, + cursor.map(|c| ScanCursor{ cursor: c, layer: 0 }), // TODO: unify cloud with OSS + precise, + project_id, + ) + .await + } + WorkerSubcommand::Update { + component_id_or_name, + worker_name, + target_version, + mode, + } => { + let (component_id_or_name, project_ref) = component_id_or_name.split(); + let project_id = projects.resolve_id_or_default_opt(project_ref).await?; + service + .update( + component_id_or_name, + worker_name, + target_version, + mode, + project_id, + ) + .await + } + } + } +} diff --git a/golem-cli/src/cloud/factory.rs b/golem-cli/src/cloud/factory.rs new file mode 100644 index 000000000..45fe9c841 --- /dev/null +++ b/golem-cli/src/cloud/factory.rs @@ -0,0 +1,366 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::api_definition::ApiDefinitionClient; +use crate::clients::api_deployment::ApiDeploymentClient; +use crate::clients::component::ComponentClient; +use crate::clients::health_check::HealthCheckClient; +use crate::clients::worker::WorkerClient; +use crate::cloud::auth::{Auth, AuthLive}; +use crate::cloud::clients::account::{AccountClient, AccountClientLive}; +use crate::cloud::clients::component::ComponentClientLive; +use crate::cloud::clients::gateway::api_definition::ApiDefinitionClientLive; +use crate::cloud::clients::gateway::api_deployment::ApiDeploymentClientLive; +use crate::cloud::clients::gateway::certificate::{CertificateClient, CertificateClientLive}; +use crate::cloud::clients::gateway::domain::{DomainClient, DomainClientLive}; +use crate::cloud::clients::grant::{GrantClient, GrantClientLive}; +use crate::cloud::clients::login::{LoginClient, LoginClientLive}; +use crate::cloud::clients::policy::{ProjectPolicyClient, ProjectPolicyClientLive}; +use crate::cloud::clients::project::{ProjectClient, ProjectClientLive}; +use crate::cloud::clients::project_grant::{ProjectGrantClient, ProjectGrantClientLive}; +use crate::cloud::clients::token::{TokenClient, TokenClientLive}; +use crate::cloud::clients::worker::WorkerClientLive; +use crate::cloud::clients::CloudAuthentication; +use crate::cloud::model::ProjectId; +use crate::cloud::service::account::{AccountService, AccountServiceLive}; +use crate::cloud::service::certificate::{CertificateService, CertificateServiceLive}; +use crate::cloud::service::domain::{DomainService, DomainServiceLive}; +use crate::cloud::service::grant::{GrantService, GrantServiceLive}; +use crate::cloud::service::policy::{ProjectPolicyService, ProjectPolicyServiceLive}; +use crate::cloud::service::project::{ProjectService, ProjectServiceLive}; +use crate::cloud::service::project_grant::{ProjectGrantService, ProjectGrantServiceLive}; +use crate::cloud::service::token::{TokenService, TokenServiceLive}; +use crate::factory::{FactoryWithAuth, ServiceFactory}; +use crate::model::GolemError; +use golem_cloud_client::{Context, Security}; +use url::Url; + +#[derive(Debug, Clone)] +pub struct CloudServiceFactory { + pub url: Url, + pub gateway_url: Url, + pub allow_insecure: bool, +} + +impl CloudServiceFactory { + fn client(&self) -> Result { + let mut builder = reqwest::Client::builder(); + if self.allow_insecure { + builder = builder.danger_accept_invalid_certs(true); + } + + Ok(builder.connection_verbose(true).build()?) + } + + fn login_context(&self) -> Result { + Ok(Context { + base_url: self.url.clone(), + client: self.client()?, + security_token: Security::Empty, + }) + } + + fn login_client(&self) -> Result, GolemError> { + Ok(Box::new(LoginClientLive { + client: golem_cloud_client::api::LoginClientLive { + context: self.login_context()?, + }, + context: self.login_context()?, + })) + } + + pub fn auth(&self) -> Result, GolemError> { + Ok(Box::new(AuthLive { + login: self.login_client()?, + })) + } + + fn context(&self, auth: &CloudAuthentication) -> Result { + Ok(Context { + base_url: self.url.clone(), + client: self.client()?, + security_token: Security::Bearer(auth.0.secret.value.to_string()), + }) + } + + fn worker_context( + &self, + auth: &CloudAuthentication, + ) -> Result { + Ok(golem_cloud_worker_client::Context { + base_url: self.gateway_url.clone(), + client: self.client()?, + security_token: golem_cloud_worker_client::Security::Bearer( + auth.0.secret.value.to_string(), + ), + }) + } + + fn project_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectClientLive { + client: golem_cloud_client::api::ProjectClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn project_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectServiceLive { + account_id: auth.account_id(), + client: self.project_client(auth)?, + })) + } + + fn account_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(AccountClientLive { + client: golem_cloud_client::api::AccountClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn account_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(AccountServiceLive { + account_id: auth.account_id(), + client: self.account_client(auth)?, + })) + } + + fn grant_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(GrantClientLive { + client: golem_cloud_client::api::GrantClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn grant_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(GrantServiceLive { + account_id: auth.account_id(), + client: self.grant_client(auth)?, + })) + } + + fn token_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(TokenClientLive { + client: golem_cloud_client::api::TokenClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn token_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(TokenServiceLive { + account_id: auth.account_id(), + client: self.token_client(auth)?, + })) + } + + fn project_grant_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectGrantClientLive { + client: golem_cloud_client::api::ProjectGrantClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn project_grant_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectGrantServiceLive { + client: self.project_grant_client(auth)?, + projects: self.project_service(auth)?, + })) + } + + fn project_policy_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectPolicyClientLive { + client: golem_cloud_client::api::ProjectPolicyClientLive { + context: self.context(auth)?, + }, + })) + } + + pub fn project_policy_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(ProjectPolicyServiceLive { + client: self.project_policy_client(auth)?, + })) + } + + fn certificate_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(CertificateClientLive { + client: golem_cloud_worker_client::api::ApiCertificateClientLive { + context: self.worker_context(auth)?, + }, + })) + } + + pub fn certificate_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(CertificateServiceLive { + client: self.certificate_client(auth)?, + projects: self.project_service(auth)?, + })) + } + + fn domain_client( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(DomainClientLive { + client: golem_cloud_worker_client::api::ApiDomainClientLive { + context: self.worker_context(auth)?, + }, + })) + } + + pub fn domain_service( + &self, + auth: &CloudAuthentication, + ) -> Result, GolemError> { + Ok(Box::new(DomainServiceLive { + client: self.domain_client(auth)?, + projects: self.project_service(auth)?, + })) + } +} + +impl ServiceFactory for CloudServiceFactory { + type SecurityContext = CloudAuthentication; + type ProjectContext = ProjectId; + + fn with_auth( + &self, + auth: &Self::SecurityContext, + ) -> FactoryWithAuth { + FactoryWithAuth { + auth: auth.clone(), + factory: Box::new(self.clone()), + } + } + + fn component_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ComponentClientLive { + client: golem_cloud_client::api::ComponentClientLive { + context: self.context(auth)?, + }, + })) + } + + fn worker_client( + &self, + auth: &Self::SecurityContext, + ) -> Result, GolemError> { + Ok(Box::new(WorkerClientLive { + client: golem_cloud_worker_client::api::WorkerClientLive { + context: self.worker_context(auth)?, + }, + context: self.worker_context(auth)?, + allow_insecure: self.allow_insecure, + })) + } + + fn api_definition_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDefinitionClientLive { + client: golem_cloud_worker_client::api::ApiDefinitionClientLive { + context: self.worker_context(auth)?, + }, + })) + } + + fn api_deployment_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDeploymentClientLive { + client: golem_cloud_worker_client::api::ApiDeploymentClientLive { + context: self.worker_context(auth)?, + }, + })) + } + + fn health_check_clients( + &self, + auth: &Self::SecurityContext, + ) -> Result>, GolemError> { + Ok(vec![ + Box::new(crate::cloud::clients::health_check::HealthCheckClientLive { + client: golem_cloud_client::api::HealthCheckClientLive { + context: self.context(auth)?, + }, + }), + Box::new( + crate::cloud::clients::gateway::health_check::HealthCheckClientLive { + client: golem_cloud_worker_client::api::HealthCheckClientLive { + context: self.worker_context(auth)?, + }, + }, + ), + ]) + } +} diff --git a/golem-cli/src/cloud/model.rs b/golem-cli/src/cloud/model.rs new file mode 100644 index 000000000..b3c94f5e7 --- /dev/null +++ b/golem-cli/src/cloud/model.rs @@ -0,0 +1,343 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod text; + +use crate::model::{ComponentId, ComponentIdOrName, ComponentName}; +use clap::{ArgMatches, Error, FromArgMatches}; +use derive_more::{Display, FromStr, Into}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use uuid::Uuid; + +#[derive(Clone, PartialEq, Eq, Debug, Display, FromStr, Serialize, Deserialize)] +pub struct AccountId { + pub id: String, +} + +impl AccountId { + pub fn new(id: String) -> AccountId { + AccountId { id } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Display, FromStr, Into)] +pub struct TokenId(pub Uuid); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Into, Serialize, Deserialize)] +pub struct ProjectId(pub Uuid); + +impl Display for ProjectId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum ProjectRef { + Id(ProjectId), + Name(String), + Default, +} + +impl FromArgMatches for ProjectRef { + fn from_arg_matches(matches: &ArgMatches) -> Result { + ProjectRefArgs::from_arg_matches(matches).map(|c| (&c).into()) + } + + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { + let prc0: ProjectRefArgs = (&self.clone()).into(); + let mut prc = prc0.clone(); + let res = ProjectRefArgs::update_from_arg_matches(&mut prc, matches); + *self = (&prc).into(); + res + } +} + +impl clap::Args for ProjectRef { + fn augment_args(cmd: clap::Command) -> clap::Command { + ProjectRefArgs::augment_args(cmd) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + ProjectRefArgs::augment_args_for_update(cmd) + } +} + +#[derive(clap::Args, Debug, Clone)] +struct ProjectRefArgs { + #[arg(short = 'P', long, conflicts_with = "project_name")] + project_id: Option, + + #[arg(short = 'p', long, conflicts_with = "project_id")] + project_name: Option, +} + +impl From<&ProjectRefArgs> for ProjectRef { + fn from(value: &ProjectRefArgs) -> ProjectRef { + if let Some(id) = value.project_id { + ProjectRef::Id(ProjectId(id)) + } else if let Some(name) = value.project_name.clone() { + ProjectRef::Name(name) + } else { + ProjectRef::Default + } + } +} + +impl From<&ProjectRef> for ProjectRefArgs { + fn from(value: &ProjectRef) -> Self { + match value { + ProjectRef::Id(ProjectId(id)) => ProjectRefArgs { + project_id: Some(*id), + project_name: None, + }, + ProjectRef::Name(name) => ProjectRefArgs { + project_id: None, + project_name: Some(name.clone()), + }, + ProjectRef::Default => ProjectRefArgs { + project_id: None, + project_name: None, + }, + } + } +} + +impl FromArgMatches for CloudComponentIdOrName { + fn from_arg_matches(matches: &ArgMatches) -> Result { + CloudComponentIdOrNameArgs::from_arg_matches(matches).map(|c| (&c).into()) + } + + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { + let prc0: CloudComponentIdOrNameArgs = (&self.clone()).into(); + let mut prc = prc0.clone(); + let res = CloudComponentIdOrNameArgs::update_from_arg_matches(&mut prc, matches); + *self = (&prc).into(); + res + } +} + +impl clap::Args for CloudComponentIdOrName { + fn augment_args(cmd: clap::Command) -> clap::Command { + CloudComponentIdOrNameArgs::augment_args(cmd) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + CloudComponentIdOrNameArgs::augment_args_for_update(cmd) + } +} + +#[derive(clap::Args, Debug, Clone)] +struct CloudComponentIdOrNameArgs { + #[arg(short = 'C', long, conflicts_with = "component_name", required = true)] + component_id: Option, + + #[arg(short = 'c', long, conflicts_with = "component_id", required = true)] + component_name: Option, + + #[arg( + short = 'P', + long, + conflicts_with = "project_name", + conflicts_with = "component_id" + )] + project_id: Option, + + #[arg( + short = 'p', + long, + conflicts_with = "project_id", + conflicts_with = "component_id" + )] + project_name: Option, +} + +impl From<&CloudComponentIdOrNameArgs> for CloudComponentIdOrName { + fn from(value: &CloudComponentIdOrNameArgs) -> CloudComponentIdOrName { + let pr = if let Some(id) = value.project_id { + ProjectRef::Id(ProjectId(id)) + } else if let Some(name) = value.project_name.clone() { + ProjectRef::Name(name) + } else { + ProjectRef::Default + }; + + if let Some(id) = value.component_id { + CloudComponentIdOrName::Id(ComponentId(id)) + } else { + CloudComponentIdOrName::Name( + ComponentName(value.component_name.as_ref().unwrap().to_string()), + pr, + ) + } + } +} + +impl From<&CloudComponentIdOrName> for CloudComponentIdOrNameArgs { + fn from(value: &CloudComponentIdOrName) -> CloudComponentIdOrNameArgs { + match value { + CloudComponentIdOrName::Id(ComponentId(id)) => CloudComponentIdOrNameArgs { + component_id: Some(*id), + component_name: None, + project_id: None, + project_name: None, + }, + CloudComponentIdOrName::Name(ComponentName(name), pr) => { + let (project_id, project_name) = match pr { + ProjectRef::Id(ProjectId(id)) => (Some(*id), None), + ProjectRef::Name(name) => (None, Some(name.to_string())), + ProjectRef::Default => (None, None), + }; + + CloudComponentIdOrNameArgs { + component_id: None, + component_name: Some(name.clone()), + project_id, + project_name, + } + } + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum CloudComponentIdOrName { + Id(ComponentId), + Name(ComponentName, ProjectRef), +} + +impl CloudComponentIdOrName { + pub fn split(self) -> (ComponentIdOrName, Option) { + match self { + CloudComponentIdOrName::Id(id) => (ComponentIdOrName::Id(id), None), + CloudComponentIdOrName::Name(name, p) => (ComponentIdOrName::Name(name), Some(p)), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, EnumIter, Serialize, Deserialize)] +pub enum Role { + Admin, + MarketingAdmin, + ViewProject, + DeleteProject, + CreateProject, + InstanceServer, +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + Role::Admin => "Admin", + Role::MarketingAdmin => "MarketingAdmin", + Role::ViewProject => "ViewProject", + Role::DeleteProject => "DeleteProject", + Role::CreateProject => "CreateProject", + Role::InstanceServer => "InstanceServer", + }; + + Display::fmt(s, f) + } +} + +impl FromStr for Role { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "Admin" => Ok(Role::Admin), + "MarketingAdmin" => Ok(Role::MarketingAdmin), + "ViewProject" => Ok(Role::ViewProject), + "DeleteProject" => Ok(Role::DeleteProject), + "CreateProject" => Ok(Role::CreateProject), + "InstanceServer" => Ok(Role::InstanceServer), + _ => { + let all = Role::iter() + .map(|x| format!("\"{x}\"")) + .collect::>() + .join(", "); + Err(format!("Unknown role: {s}. Expected one of {all}")) + } + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, EnumIter)] +pub enum ProjectAction { + ViewComponent, + CreateComponent, + UpdateComponent, + DeleteComponent, + ViewWorker, + CreateWorker, + UpdateWorker, + DeleteWorker, + ViewProjectGrants, + CreateProjectGrants, + DeleteProjectGrants, +} + +impl Display for ProjectAction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + ProjectAction::ViewComponent => "ViewComponent", + ProjectAction::CreateComponent => "CreateComponent", + ProjectAction::UpdateComponent => "UpdateComponent", + ProjectAction::DeleteComponent => "DeleteComponent", + ProjectAction::ViewWorker => "ViewWorker", + ProjectAction::CreateWorker => "CreateWorker", + ProjectAction::UpdateWorker => "UpdateWorker", + ProjectAction::DeleteWorker => "DeleteWorker", + ProjectAction::ViewProjectGrants => "ViewProjectGrants", + ProjectAction::CreateProjectGrants => "CreateProjectGrants", + ProjectAction::DeleteProjectGrants => "DeleteProjectGrants", + }; + + Display::fmt(s, f) + } +} + +impl FromStr for ProjectAction { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "ViewComponent" => Ok(ProjectAction::ViewComponent), + "CreateComponent" => Ok(ProjectAction::CreateComponent), + "UpdateComponent" => Ok(ProjectAction::UpdateComponent), + "DeleteComponent" => Ok(ProjectAction::DeleteComponent), + "ViewWorker" => Ok(ProjectAction::ViewWorker), + "CreateWorker" => Ok(ProjectAction::CreateWorker), + "UpdateWorker" => Ok(ProjectAction::UpdateWorker), + "DeleteWorker" => Ok(ProjectAction::DeleteWorker), + "ViewProjectGrants" => Ok(ProjectAction::ViewProjectGrants), + "CreateProjectGrants" => Ok(ProjectAction::CreateProjectGrants), + "DeleteProjectGrants" => Ok(ProjectAction::DeleteProjectGrants), + _ => { + let all = ProjectAction::iter() + .map(|x| format!("\"{x}\"")) + .collect::>() + .join(", "); + Err(format!("Unknown action: {s}. Expected one of {all}")) + } + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Display, FromStr)] +pub struct ProjectPolicyId(pub Uuid); diff --git a/golem-cli/src/cloud/model/text.rs b/golem-cli/src/cloud/model/text.rs new file mode 100644 index 000000000..732e717c3 --- /dev/null +++ b/golem-cli/src/cloud/model/text.rs @@ -0,0 +1,300 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::model::Role; +use crate::model::text::TextFormat; +use chrono::{DateTime, Utc}; +use cli_table::{print_stdout, Table, WithTitle}; +use colored::Colorize; +use golem_cloud_client::model::{ + Account, Project, ProjectGrant, ProjectPolicy, Token, UnsafeToken, +}; +use golem_cloud_worker_client::model::{ApiDomain, Certificate}; +use indoc::printdoc; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +fn print_account(account: &Account, action: &str) { + printdoc!( + " + Account{action} with id {} for name {} with email {}. + ", + account.id, + account.name, + account.email, + ) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccountViewGet(pub Account); + +impl TextFormat for AccountViewGet { + fn print(&self) { + print_account(&self.0, "") + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccountViewAdd(pub Account); + +impl TextFormat for AccountViewAdd { + fn print(&self) { + print_account(&self.0, " created") + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccountViewUpdate(pub Account); + +impl TextFormat for AccountViewUpdate { + fn print(&self) { + print_account(&self.0, " updated") + } +} + +impl TextFormat for Project { + fn print(&self) { + printdoc!( + r#" + Project "{}" with id {}. + Description: "{}". + Owner: {}, environment: {}, type: {} + "#, + self.project_data.name, + self.project_id, + self.project_data.description, + self.project_data.owner_account_id, + self.project_data.default_environment_id, + self.project_data.project_type, + ) + } +} + +#[derive(Table)] +struct ProjectListView { + #[table(title = "ID")] + pub id: Uuid, + #[table(title = "Name")] + pub name: String, + #[table(title = "Description")] + pub description: String, +} + +impl From<&Project> for ProjectListView { + fn from(value: &Project) -> Self { + ProjectListView { + id: value.project_id, + name: value.project_data.name.to_string(), + description: textwrap::wrap(&value.project_data.description, 30).join("\n"), + } + } +} + +impl TextFormat for Vec { + fn print(&self) { + print_stdout( + self.iter() + .map(ProjectListView::from) + .collect::>() + .with_title(), + ) + .unwrap() + } +} + +impl TextFormat for Vec { + fn print(&self) { + println!( + "Available roles: {}.", + self.iter().map(|r| r.to_string()).join(", ") + ) + } +} + +impl TextFormat for UnsafeToken { + fn print(&self) { + printdoc!( + " + New token created with id {} and expiration date {}. + Please save this token secret, you can't get this data later: + {} + ", + self.data.id, + self.data.expires_at, + self.secret.value.to_string().bold() + ) + } +} + +#[derive(Table)] +struct TokenListView { + #[table(title = "ID")] + pub id: Uuid, + #[table(title = "Created at")] + pub created_at: DateTime, + #[table(title = "Expires at")] + pub expires_at: DateTime, + #[table(title = "Account")] + pub account_id: String, +} + +impl From<&Token> for TokenListView { + fn from(value: &Token) -> Self { + TokenListView { + id: value.id, + created_at: value.created_at, + expires_at: value.expires_at, + account_id: value.account_id.to_string(), + } + } +} + +impl TextFormat for Vec { + fn print(&self) { + print_stdout( + self.iter() + .map(TokenListView::from) + .collect::>() + .with_title(), + ) + .unwrap() + } +} + +impl TextFormat for ProjectGrant { + fn print(&self) { + printdoc!( + " + Project grant {}. + Account: {}. + Project: {}. + Policy: {} + ", + self.id, + self.data.grantee_account_id, + self.data.grantor_project_id, + self.data.project_policy_id, + ) + } +} + +impl TextFormat for ProjectPolicy { + fn print(&self) { + printdoc!( + " + Project policy {} with id {}. + Actions: {}. + ", + self.name, + self.id, + self.project_actions + .actions + .iter() + .map(|a| a.to_string()) + .join(", ") + ) + } +} + +impl TextFormat for Certificate { + fn print(&self) { + printdoc!( + " + Certificate with id {} for domain {} on project {}. + ", + self.id, + self.domain_name, + self.project_id + ) + } +} + +#[derive(Table)] +struct CertificateListView { + #[table(title = "Domain")] + pub domain_name: String, + #[table(title = "ID")] + pub id: Uuid, + #[table(title = "Project")] + pub project_id: Uuid, +} + +impl From<&Certificate> for CertificateListView { + fn from(value: &Certificate) -> Self { + CertificateListView { + domain_name: value.domain_name.to_string(), + id: value.id, + project_id: value.project_id, + } + } +} + +impl TextFormat for Vec { + fn print(&self) { + print_stdout( + self.iter() + .map(CertificateListView::from) + .collect::>() + .with_title(), + ) + .unwrap() + } +} + +impl TextFormat for ApiDomain { + fn print(&self) { + printdoc!( + " + Domain {} on project {}. + Servers: {}. + ", + self.domain_name, + self.project_id, + self.name_servers.join(", ") + ) + } +} + +#[derive(Table)] +struct DomainListView { + #[table(title = "Domain")] + pub domain_name: String, + #[table(title = "Project")] + pub project_id: Uuid, + #[table(title = "Servers")] + pub name_servers: String, +} + +impl From<&ApiDomain> for DomainListView { + fn from(value: &ApiDomain) -> Self { + DomainListView { + domain_name: value.domain_name.to_string(), + project_id: value.project_id, + name_servers: value.name_servers.join("\n"), + } + } +} + +impl TextFormat for Vec { + fn print(&self) { + print_stdout( + self.iter() + .map(DomainListView::from) + .collect::>() + .with_title(), + ) + .unwrap() + } +} diff --git a/golem-cli/src/cloud/service.rs b/golem-cli/src/cloud/service.rs new file mode 100644 index 000000000..881640db2 --- /dev/null +++ b/golem-cli/src/cloud/service.rs @@ -0,0 +1,22 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod account; +pub mod certificate; +pub mod domain; +pub mod grant; +pub mod policy; +pub mod project; +pub mod project_grant; +pub mod token; diff --git a/golem-cli/src/cloud/service/account.rs b/golem-cli/src/cloud/service/account.rs new file mode 100644 index 000000000..3ad47074e --- /dev/null +++ b/golem-cli/src/cloud/service/account.rs @@ -0,0 +1,87 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::account::AccountClient; +use crate::cloud::model::text::{AccountViewAdd, AccountViewGet, AccountViewUpdate}; +use crate::cloud::model::AccountId; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; +use golem_cloud_client::model::AccountData; + +#[async_trait] +pub trait AccountService { + async fn get(&self, account_id: Option) -> Result; + async fn update( + &self, + account_name: Option, + account_email: Option, + account_id: Option, + ) -> Result; + async fn add( + &self, + account_name: String, + account_email: String, + ) -> Result; + async fn delete(&self, account_id: Option) -> Result; +} + +pub struct AccountServiceLive { + pub account_id: AccountId, + pub client: Box, +} + +#[async_trait] +impl AccountService for AccountServiceLive { + async fn get(&self, account_id: Option) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + let account = self.client.get(account_id).await?; + Ok(GolemResult::Ok(Box::new(AccountViewGet(account)))) + } + + async fn update( + &self, + account_name: Option, + account_email: Option, + account_id: Option, + ) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + let existing = self.client.get(account_id).await?; + let name = account_name.unwrap_or(existing.name); + let email = account_email.unwrap_or(existing.email); + let updated = AccountData { name, email }; + let account = self.client.put(account_id, updated).await?; + Ok(GolemResult::Ok(Box::new(AccountViewUpdate(account)))) + } + + async fn add( + &self, + account_name: String, + account_email: String, + ) -> Result { + let data = AccountData { + name: account_name, + email: account_email, + }; + + let account = self.client.post(data).await?; + + Ok(GolemResult::Ok(Box::new(AccountViewAdd(account)))) + } + + async fn delete(&self, account_id: Option) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + self.client.delete(account_id).await?; + Ok(GolemResult::Str("Deleted".to_string())) + } +} diff --git a/golem-cli/src/cloud/service/certificate.rs b/golem-cli/src/cloud/service/certificate.rs new file mode 100644 index 000000000..6da7920c2 --- /dev/null +++ b/golem-cli/src/cloud/service/certificate.rs @@ -0,0 +1,119 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::gateway::certificate::CertificateClient; +use crate::cloud::model::ProjectRef; +use crate::cloud::service::project::ProjectService; +use crate::model::{GolemError, GolemResult, PathBufOrStdin}; +use async_trait::async_trait; +use golem_cloud_worker_client::model::CertificateRequest; +use std::fs::File; +use std::io; +use std::io::{BufReader, Read}; +use uuid::Uuid; + +#[async_trait] +pub trait CertificateService { + async fn get( + &self, + project_ref: ProjectRef, + certificate_id: Option, + ) -> Result; + async fn add( + &self, + project_ref: ProjectRef, + domain_name: String, + certificate_body: PathBufOrStdin, + certificate_private_key: PathBufOrStdin, + ) -> Result; + async fn delete( + &self, + project_ref: ProjectRef, + certificate_id: Uuid, + ) -> Result; +} + +pub struct CertificateServiceLive { + pub client: Box, + pub projects: Box, +} + +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 CertificateService for CertificateServiceLive { + async fn get( + &self, + project_ref: ProjectRef, + certificate_id: Option, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + + let res = self.client.get(project_id, certificate_id.as_ref()).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + + async fn add( + &self, + project_ref: ProjectRef, + domain_name: String, + certificate_body: PathBufOrStdin, + certificate_private_key: PathBufOrStdin, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).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))) + } + + async fn delete( + &self, + project_ref: ProjectRef, + certificate_id: Uuid, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + let res = self.client.delete(project_id, &certificate_id).await?; + Ok(GolemResult::Str(res)) + } +} diff --git a/golem-cli/src/cloud/service/domain.rs b/golem-cli/src/cloud/service/domain.rs new file mode 100644 index 000000000..24e4264bc --- /dev/null +++ b/golem-cli/src/cloud/service/domain.rs @@ -0,0 +1,72 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::gateway::domain::DomainClient; +use crate::cloud::model::ProjectRef; +use crate::cloud::service::project::ProjectService; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; + +#[async_trait] +pub trait DomainService { + async fn get(&self, project_ref: ProjectRef) -> Result; + async fn add( + &self, + project_ref: ProjectRef, + domain_name: String, + ) -> Result; + async fn delete( + &self, + project_ref: ProjectRef, + domain_name: String, + ) -> Result; +} + +pub struct DomainServiceLive { + pub client: Box, + pub projects: Box, +} + +#[async_trait] +impl DomainService for DomainServiceLive { + async fn get(&self, project_ref: ProjectRef) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + + let res = self.client.get(project_id).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + + async fn add( + &self, + project_ref: ProjectRef, + domain_name: String, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + + let res = self.client.update(project_id, domain_name).await?; + + Ok(GolemResult::Ok(Box::new(res))) + } + + async fn delete( + &self, + project_ref: ProjectRef, + domain_name: String, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + let res = self.client.delete(project_id, &domain_name).await?; + Ok(GolemResult::Str(res)) + } +} diff --git a/golem-cli/src/cloud/service/grant.rs b/golem-cli/src/cloud/service/grant.rs new file mode 100644 index 000000000..77f2bfbb7 --- /dev/null +++ b/golem-cli/src/cloud/service/grant.rs @@ -0,0 +1,70 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::grant::GrantClient; +use crate::cloud::model::{AccountId, Role}; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; + +#[async_trait] +pub trait GrantService { + async fn get(&self, account_id: Option) -> Result; + async fn add( + &self, + role: Role, + account_id: Option, + ) -> Result; + async fn delete( + &self, + role: Role, + account_id: Option, + ) -> Result; +} + +pub struct GrantServiceLive { + pub account_id: AccountId, + pub client: Box, +} + +#[async_trait] +impl GrantService for GrantServiceLive { + async fn get(&self, account_id: Option) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + let roles = self.client.get_all(account_id).await?; + + Ok(GolemResult::Ok(Box::new(roles))) + } + + async fn add( + &self, + role: Role, + account_id: Option, + ) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + self.client.put(account_id, role).await?; + + Ok(GolemResult::Str("Role granted".to_string())) + } + + async fn delete( + &self, + role: Role, + account_id: Option, + ) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + self.client.delete(account_id, role).await?; + + Ok(GolemResult::Str("Role removed".to_string())) + } +} diff --git a/golem-cli/src/cloud/service/policy.rs b/golem-cli/src/cloud/service/policy.rs new file mode 100644 index 000000000..a30c04b32 --- /dev/null +++ b/golem-cli/src/cloud/service/policy.rs @@ -0,0 +1,54 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::policy::ProjectPolicyClient; +use crate::cloud::model::{ProjectAction, ProjectPolicyId}; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; + +#[async_trait] +pub trait ProjectPolicyService { + async fn add( + &self, + project_policy_name: String, + project_actions: Vec, + ) -> Result; + async fn get(&self, project_policy_id: ProjectPolicyId) -> Result; +} + +pub struct ProjectPolicyServiceLive { + pub client: Box, +} + +#[async_trait] +impl ProjectPolicyService for ProjectPolicyServiceLive { + async fn add( + &self, + project_policy_name: String, + project_actions: Vec, + ) -> Result { + let policy = self + .client + .create(project_policy_name, project_actions) + .await?; + + Ok(GolemResult::Ok(Box::new(policy))) + } + + async fn get(&self, project_policy_id: ProjectPolicyId) -> Result { + let policy = self.client.get(project_policy_id).await?; + + Ok(GolemResult::Ok(Box::new(policy))) + } +} diff --git a/golem-cli/src/cloud/service/project.rs b/golem-cli/src/cloud/service/project.rs new file mode 100644 index 000000000..fccc5d4a8 --- /dev/null +++ b/golem-cli/src/cloud/service/project.rs @@ -0,0 +1,119 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::project::ProjectClient; +use crate::cloud::model::{AccountId, ProjectId, ProjectRef}; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; +use golem_cloud_client::model::Project; +use indoc::formatdoc; + +#[async_trait] +pub trait ProjectService { + async fn add( + &self, + project_name: String, + project_description: Option, + ) -> Result; + async fn list(&self, project_name: Option) -> Result; + async fn get_default(&self) -> Result; + + async fn find_default(&self) -> Result; + async fn resolve_id(&self, project_ref: ProjectRef) -> Result, GolemError>; + + async fn resolve_id_or_default( + &self, + project_ref: ProjectRef, + ) -> Result { + match self.resolve_id(project_ref).await? { + None => Ok(ProjectId(self.find_default().await?.project_id)), + Some(project_id) => Ok(project_id), + } + } + + async fn resolve_id_or_default_opt( + &self, + project_ref: Option, + ) -> Result, GolemError> { + match project_ref { + None => Ok(None), + Some(project_ref) => Ok(Some(self.resolve_id_or_default(project_ref).await?)), + } + } +} + +pub struct ProjectServiceLive { + pub account_id: AccountId, + pub client: Box, +} + +#[async_trait] +impl ProjectService for ProjectServiceLive { + async fn add( + &self, + project_name: String, + project_description: Option, + ) -> Result { + let project = self + .client + .create(&self.account_id, project_name, project_description) + .await?; + + Ok(GolemResult::Ok(Box::new(project))) + } + + async fn list(&self, project_name: Option) -> Result { + let projects = self.client.find(project_name).await?; + + Ok(GolemResult::Ok(Box::new(projects))) + } + + async fn get_default(&self) -> Result { + let project = self.find_default().await?; + + Ok(GolemResult::Ok(Box::new(project))) + } + + async fn find_default(&self) -> Result { + self.client.find_default().await + } + + async fn resolve_id(&self, project_ref: ProjectRef) -> Result, GolemError> { + match project_ref { + ProjectRef::Id(id) => Ok(Some(id)), + ProjectRef::Name(name) => { + let projects = self.client.find(Some(name.clone())).await?; + + if projects.len() > 1 { + let projects: Vec = + projects.iter().map(|p| p.project_id.to_string()).collect(); + Err(GolemError(formatdoc!( + " + Multiple projects found for name {name}: + {} + Use explicit --project-id or set target project as default. + ", + projects.join(", ") + ))) + } else { + match projects.first() { + None => Err(GolemError(format!("Can't find project with name {name}"))), + Some(project) => Ok(Some(ProjectId(project.project_id))), + } + } + } + ProjectRef::Default => Ok(None), + } + } +} diff --git a/golem-cli/src/cloud/service/project_grant.rs b/golem-cli/src/cloud/service/project_grant.rs new file mode 100644 index 000000000..3c4da654f --- /dev/null +++ b/golem-cli/src/cloud/service/project_grant.rs @@ -0,0 +1,68 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::project_grant::ProjectGrantClient; +use crate::cloud::model::{AccountId, ProjectAction, ProjectPolicyId, ProjectRef}; +use crate::cloud::service::project::ProjectService; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; + +#[async_trait] +pub trait ProjectGrantService { + async fn grant( + &self, + project_ref: ProjectRef, + recipient_account_id: AccountId, + project_policy_id: Option, + project_actions: Option>, + ) -> Result; +} + +pub struct ProjectGrantServiceLive { + pub client: Box, + pub projects: Box, +} + +#[async_trait] +impl ProjectGrantService for ProjectGrantServiceLive { + async fn grant( + &self, + project_ref: ProjectRef, + recipient_account_id: AccountId, + project_policy_id: Option, + project_actions: Option>, + ) -> Result { + let project_id = self.projects.resolve_id_or_default(project_ref).await?; + match project_policy_id { + None => { + let actions = project_actions.unwrap(); + + let grant = self + .client + .create_actions(project_id, recipient_account_id, actions) + .await?; + + Ok(GolemResult::Ok(Box::new(grant))) + } + Some(policy_id) => { + let grant = self + .client + .create(project_id, recipient_account_id, policy_id) + .await?; + + Ok(GolemResult::Ok(Box::new(grant))) + } + } + } +} diff --git a/golem-cli/src/cloud/service/token.rs b/golem-cli/src/cloud/service/token.rs new file mode 100644 index 000000000..ef9b737e9 --- /dev/null +++ b/golem-cli/src/cloud/service/token.rs @@ -0,0 +1,68 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cloud::clients::token::TokenClient; +use crate::cloud::model::{AccountId, TokenId}; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; + +#[async_trait] +pub trait TokenService { + async fn list(&self, account_id: Option) -> Result; + async fn add( + &self, + expires_at: DateTime, + account_id: Option, + ) -> Result; + async fn delete( + &self, + token_id: TokenId, + account_id: Option, + ) -> Result; +} + +pub struct TokenServiceLive { + pub account_id: AccountId, + pub client: Box, +} + +#[async_trait] +impl TokenService for TokenServiceLive { + async fn list(&self, account_id: Option) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + let token = self.client.get_all(account_id).await?; + Ok(GolemResult::Ok(Box::new(token))) + } + + async fn add( + &self, + expires_at: DateTime, + account_id: Option, + ) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + let token = self.client.post(account_id, expires_at).await?; + Ok(GolemResult::Ok(Box::new(token))) + } + + async fn delete( + &self, + token_id: TokenId, + account_id: Option, + ) -> Result { + let account_id = account_id.as_ref().unwrap_or(&self.account_id); + self.client.delete(account_id, token_id).await?; + Ok(GolemResult::Str("Deleted".to_string())) + } +} diff --git a/golem-cli/src/cloud_main.rs b/golem-cli/src/cloud_main.rs new file mode 100644 index 000000000..72e5aa761 --- /dev/null +++ b/golem-cli/src/cloud_main.rs @@ -0,0 +1,204 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Parser; +use clap_verbosity_flag::Level; +use golem_cli::cloud::command::{CloudCommand, GolemCloudCommand}; +use golem_cli::cloud::factory::CloudServiceFactory; +use golem_cli::examples; +use golem_cli::factory::ServiceFactory; +use golem_cli::stubgen::handle_stubgen; +use tracing::debug; +use tracing_subscriber::FmtSubscriber; +use url::Url; + +fn main() -> Result<(), Box> { + let command = GolemCloudCommand::parse(); + + if let Some(level) = command.verbosity.log_level() { + let tracing_level = match level { + Level::Error => tracing::Level::ERROR, + Level::Warn => tracing::Level::WARN, + Level::Info => tracing::Level::INFO, + Level::Debug => tracing::Level::DEBUG, + Level::Trace => tracing::Level::TRACE, + }; + + let subscriber = FmtSubscriber::builder() + .with_max_level(tracing_level) + .with_writer(std::io::stderr) + .finish(); + + tracing::subscriber::set_global_default(subscriber) + .expect("setting default subscriber failed"); + } + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async_main(command)) +} + +async fn async_main(cmd: GolemCloudCommand) -> Result<(), Box> { + let url_str = std::env::var("GOLEM_CLOUD_BASE_URL") + .ok() + .or_else(|| std::env::var("GOLEM_BASE_URL").ok()) + .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"; + let default_conf_dir = home.join(".golem"); + + debug!( + "Golem configuration directory: {}", + default_conf_dir.display() + ); + + let factory = CloudServiceFactory { + url, + gateway_url, + allow_insecure, + }; + + let auth = factory + .auth()? + .authenticate( + cmd.auth_token, + cmd.config_directory.clone().unwrap_or(default_conf_dir), + ) + .await?; + + let yellow = "\x1b[33m"; + let reset_color = "\x1b[0m"; + + let version_check = factory.version_service(&auth)?.check().await; + + if let Err(err) = version_check { + eprintln!("{}{}{}", yellow, err.0, reset_color) + } + + let res = match cmd.command { + CloudCommand::Component { subcommand } => { + subcommand + .handle( + factory.component_service(&auth)?.as_ref(), + factory.project_service(&auth)?.as_ref(), + ) + .await + } + CloudCommand::Worker { subcommand } => { + subcommand + .handle( + cmd.format, + factory.worker_service(&auth)?.as_ref(), + factory.project_service(&auth)?.as_ref(), + ) + .await + } + CloudCommand::Account { + account_id, + subcommand, + } => { + subcommand + .handle( + account_id, + factory.account_service(&auth)?.as_ref(), + factory.grant_service(&auth)?.as_ref(), + ) + .await + } + CloudCommand::Token { + account_id, + subcommand, + } => { + subcommand + .handle(account_id, factory.token_service(&auth)?.as_ref()) + .await + } + CloudCommand::Project { subcommand } => { + subcommand + .handle(factory.project_service(&auth)?.as_ref()) + .await + } + CloudCommand::Share { + project_ref, + recipient_account_id, + project_policy_id, + project_actions, + } => { + factory + .project_grant_service(&auth)? + .grant( + project_ref, + recipient_account_id, + project_policy_id, + project_actions, + ) + .await + } + CloudCommand::ProjectPolicy { subcommand } => { + subcommand + .handle(factory.project_policy_service(&auth)?.as_ref()) + .await + } + CloudCommand::New { + example, + package_name, + component_name, + } => examples::process_new(example, component_name, package_name), + CloudCommand::ListExamples { min_tier, language } => { + examples::process_list_examples(min_tier, language) + } + #[cfg(feature = "stubgen")] + CloudCommand::Stubgen { subcommand } => handle_stubgen(subcommand).await, + CloudCommand::ApiDefinition { subcommand } => { + subcommand + .handle( + factory.api_definition_service(&auth)?.as_ref(), + factory.project_service(&auth)?.as_ref(), + ) + .await + } + CloudCommand::ApiDeployment { subcommand } => { + subcommand + .handle( + factory.api_deployment_service(&auth)?.as_ref(), + factory.project_service(&auth)?.as_ref(), + ) + .await + } + CloudCommand::Certificate { subcommand } => { + subcommand + .handle(factory.certificate_service(&auth)?.as_ref()) + .await + } + CloudCommand::Domain { subcommand } => { + subcommand + .handle(factory.domain_service(&auth)?.as_ref()) + .await + } + }; + + match res { + Ok(res) => { + res.print(cmd.format); + Ok(()) + } + Err(err) => Err(Box::new(err)), + } +} diff --git a/golem-cli/src/component.rs b/golem-cli/src/component.rs deleted file mode 100644 index 60e9fd1f2..000000000 --- a/golem-cli/src/component.rs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2024 Golem Cloud -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use async_trait::async_trait; -use clap::Subcommand; -use golem_client::model::Component; -use indoc::formatdoc; -use itertools::Itertools; - -use crate::clients::component::ComponentClient; -use crate::model::component::ComponentView; -use crate::model::text::{ComponentAddView, ComponentGetView, ComponentUpdateView}; -use crate::model::{ - ComponentId, ComponentIdOrName, ComponentName, GolemError, GolemResult, PathBufOrStdin, -}; - -#[derive(Subcommand, Debug)] -#[command()] -pub enum ComponentSubCommand { - /// Creates a new component with a given name by uploading the component WASM - #[command()] - Add { - /// Name of the newly created component - #[arg(short, long)] - component_name: ComponentName, - - /// The WASM file to be used as a Golem component - #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] - component_file: PathBufOrStdin, // TODO: validate exists - }, - - /// Updates an existing component by uploading a new version of its WASM - #[command()] - Update { - /// The component name or identifier to update - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// The WASM file to be used as as a new version of the Golem component - #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] - component_file: PathBufOrStdin, // TODO: validate exists - }, - - /// Lists the existing components - #[command()] - List { - /// Optionally look for only components matching a given name - #[arg(short, long)] - component_name: Option, - }, - /// Get component - #[command()] - Get { - /// The Golem component id or name - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// The version of the component - #[arg(short = 't', long)] - version: Option, - }, -} - -#[async_trait] -pub trait ComponentHandler { - async fn handle(&self, subcommand: ComponentSubCommand) -> Result; - - async fn resolve_id(&self, reference: ComponentIdOrName) -> Result; - - async fn get_metadata( - &self, - component_id: &ComponentId, - version: u64, - ) -> Result; - - async fn get_latest_metadata( - &self, - component_id: &ComponentId, - ) -> Result; -} - -pub struct ComponentHandlerLive { - pub client: C, -} - -#[async_trait] -impl ComponentHandler for ComponentHandlerLive { - async fn handle(&self, subcommand: ComponentSubCommand) -> Result { - match subcommand { - ComponentSubCommand::Add { - component_name, - component_file, - } => { - let component = self.client.add(component_name, component_file).await?; - let view: ComponentView = component.into(); - - Ok(GolemResult::Ok(Box::new(ComponentAddView(view)))) - } - ComponentSubCommand::Update { - component_id_or_name, - component_file, - } => { - let id = self.resolve_id(component_id_or_name).await?; - let component = self.client.update(id, component_file).await?; - let view: ComponentView = component.into(); - - Ok(GolemResult::Ok(Box::new(ComponentUpdateView(view)))) - } - ComponentSubCommand::List { component_name } => { - let components = self.client.find(component_name).await?; - let views: Vec = components.into_iter().map(|t| t.into()).collect(); - - Ok(GolemResult::Ok(Box::new(views))) - } - ComponentSubCommand::Get { - component_id_or_name, - version, - } => { - let component_id = self.resolve_id(component_id_or_name).await?; - let component = match version { - Some(v) => self.get_metadata(&component_id, v).await?, - None => self.get_latest_metadata(&component_id).await?, - }; - let view: ComponentView = component.into(); - Ok(GolemResult::Ok(Box::new(ComponentGetView(view)))) - } - } - } - - async fn resolve_id(&self, reference: ComponentIdOrName) -> Result { - match reference { - ComponentIdOrName::Id(id) => Ok(id), - ComponentIdOrName::Name(name) => { - let components = self.client.find(Some(name.clone())).await?; - let components: Vec = components - .into_iter() - .group_by(|c| c.versioned_component_id.component_id) - .into_iter() - .map(|(_, group)| { - group - .max_by_key(|c| c.versioned_component_id.version) - .unwrap() - }) - .collect(); - - if components.len() > 1 { - let component_name = name.0; - let ids: Vec = components - .into_iter() - .map(|c| c.versioned_component_id.component_id.to_string()) - .collect(); - Err(GolemError(formatdoc!( - " - Multiple components found for name {component_name}: - {} - Use explicit --component-id - ", - ids.join(", ") - ))) - } else { - match components.first() { - None => { - let component_name = name.0; - Err(GolemError(format!("Can't find component {component_name}"))) - } - Some(component) => { - Ok(ComponentId(component.versioned_component_id.component_id)) - } - } - } - } - } - } - - async fn get_metadata( - &self, - component_id: &ComponentId, - version: u64, - ) -> Result { - self.client.get_metadata(component_id, version).await - } - - async fn get_latest_metadata( - &self, - component_id: &ComponentId, - ) -> Result { - self.client.get_latest_metadata(component_id).await - } -} diff --git a/golem-cli/src/factory.rs b/golem-cli/src/factory.rs new file mode 100644 index 000000000..21e7b330a --- /dev/null +++ b/golem-cli/src/factory.rs @@ -0,0 +1,154 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::api_definition::ApiDefinitionClient; +use crate::clients::api_deployment::ApiDeploymentClient; +use crate::clients::component::ComponentClient; +use crate::clients::health_check::HealthCheckClient; +use crate::clients::worker::WorkerClient; +use crate::model::GolemError; +use crate::service::api_definition::{ApiDefinitionService, ApiDefinitionServiceLive}; +use crate::service::api_deployment::{ApiDeploymentService, ApiDeploymentServiceLive}; +use crate::service::component::{ComponentService, ComponentServiceLive}; +use crate::service::version::{VersionService, VersionServiceLive}; +use crate::service::worker::{ + ComponentServiceBuilder, WorkerClientBuilder, WorkerService, WorkerServiceLive, +}; +use std::fmt::Display; + +pub trait ServiceFactory { + type SecurityContext: Clone + Send + Sync + 'static; + type ProjectContext: Display + Send + Sync + 'static; + + fn with_auth( + &self, + auth: &Self::SecurityContext, + ) -> FactoryWithAuth; + fn component_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + >; + fn component_service( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ComponentServiceLive { + client: self.component_client(auth)?, + })) + } + fn worker_client( + &self, + auth: &Self::SecurityContext, + ) -> Result, GolemError>; + fn worker_service( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > + where + Self: Send + Sync + Sized + 'static, + { + Ok(Box::new(WorkerServiceLive { + client: self.worker_client(auth)?, + components: self.component_service(auth)?, + client_builder: Box::new(self.with_auth(auth)), + component_service_builder: Box::new(self.with_auth(auth)), + })) + } + fn api_definition_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + >; + fn api_definition_service( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDefinitionServiceLive { + client: self.api_definition_client(auth)?, + })) + } + fn api_deployment_client( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + >; + fn api_deployment_service( + &self, + auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDeploymentServiceLive { + client: self.api_deployment_client(auth)?, + })) + } + + fn health_check_clients( + &self, + auth: &Self::SecurityContext, + ) -> Result>, GolemError>; + fn version_service( + &self, + auth: &Self::SecurityContext, + ) -> Result, GolemError> { + Ok(Box::new(VersionServiceLive { + clients: self.health_check_clients(auth)?, + })) + } +} + +pub struct FactoryWithAuth< + PC: Send + Sync + 'static, + SecurityContext: Clone + Send + Sync + 'static, +> { + pub auth: SecurityContext, + pub factory: Box< + dyn ServiceFactory + Send + Sync, + >, +} + +impl WorkerClientBuilder + for FactoryWithAuth +{ + fn build(&self) -> Result, GolemError> { + self.factory.worker_client(&self.auth) + } +} + +impl ComponentServiceBuilder + for FactoryWithAuth +{ + fn build( + &self, + ) -> Result + Send + Sync>, GolemError> { + self.factory.component_service(&self.auth) + } +} diff --git a/golem-cli/src/lib.rs b/golem-cli/src/lib.rs index b39bf305d..54a8a0767 100644 --- a/golem-cli/src/lib.rs +++ b/golem-cli/src/lib.rs @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod api_definition; -pub mod api_deployment; pub mod clients; -pub mod component; +pub mod cloud; pub mod examples; +pub mod factory; pub mod model; -pub mod version; -pub mod worker; +pub mod oss; +pub mod service; +pub mod stubgen; pub fn parse_key_val( s: &str, diff --git a/golem-cli/src/main.rs b/golem-cli/src/main.rs index f3f4d7bde..a36d8085f 100644 --- a/golem-cli/src/main.rs +++ b/golem-cli/src/main.rs @@ -14,125 +14,20 @@ extern crate derive_more; -use std::fmt::Debug; - -use clap::{Parser, Subcommand}; -use clap_verbosity_flag::{Level, Verbosity}; -use golem_cli::model::*; -use golem_client::Context; -use golem_examples::model::{ExampleName, GuestLanguage, GuestLanguageTier, PackageName}; +use clap::Parser; +use clap_verbosity_flag::Level; use reqwest::Url; use tracing_subscriber::FmtSubscriber; -use golem_cli::api_definition::{ - ApiDefinitionHandler, ApiDefinitionHandlerLive, ApiDefinitionSubcommand, -}; -use golem_cli::api_deployment::{ - ApiDeploymentHandler, ApiDeploymentHandlerLive, ApiDeploymentSubcommand, -}; -use golem_cli::clients::api_definition::ApiDefinitionClientLive; -use golem_cli::clients::api_deployment::ApiDeploymentClientLive; -use golem_cli::clients::component::ComponentClientLive; -use golem_cli::clients::health_check::HealthCheckClientLive; -use golem_cli::clients::worker::WorkerClientLive; -use golem_cli::component::{ComponentHandler, ComponentHandlerLive, ComponentSubCommand}; use golem_cli::examples; -use golem_cli::version::{VersionHandler, VersionHandlerLive}; -use golem_cli::worker::{WorkerHandler, WorkerHandlerLive, WorkerSubcommand}; - -#[derive(Subcommand, Debug)] -#[command()] -enum Command { - /// Upload and manage Golem components - #[command()] - Component { - #[command(subcommand)] - subcommand: ComponentSubCommand, - }, - - /// Manage Golem workers - #[command()] - Worker { - #[command(subcommand)] - subcommand: WorkerSubcommand, - }, - - /// Create a new Golem component from built-in examples - #[command()] - New { - /// Name of the example to use - #[arg(short, long)] - example: ExampleName, - - /// The new component's name - #[arg(short, long)] - component_name: golem_examples::model::ComponentName, - - /// The package name of the generated component (in namespace:name format) - #[arg(short, long)] - package_name: Option, - }, - - /// Lists the built-in examples available for creating new components - #[command()] - ListExamples { - /// The minimum language tier to include in the list - #[arg(short, long)] - min_tier: Option, - - /// Filter examples by a given guest language - #[arg(short, long)] - language: Option, - }, - - /// WASM RPC stub generator - #[cfg(feature = "stubgen")] - Stubgen { - #[command(subcommand)] - subcommand: golem_wasm_rpc_stubgen::Command, - }, - - /// Manage Golem api definitions - #[command()] - ApiDefinition { - #[command(subcommand)] - subcommand: ApiDefinitionSubcommand, - }, - - /// Manage Golem api deployments - #[command()] - ApiDeployment { - #[command(subcommand)] - subcommand: ApiDeploymentSubcommand, - }, -} - -#[derive(Parser, Debug)] -#[command(author, version = option_env ! ("VERSION").unwrap_or(env ! ("CARGO_PKG_VERSION")), about, long_about, rename_all = "kebab-case")] -/// Command line interface for OSS version of Golem. -/// -/// For Golem Cloud client see golem-cloud-cli instead: https://github.com/golemcloud/golem-cloud-cli -struct GolemCommand { - #[command(flatten)] - verbosity: Verbosity, - - #[arg(short = 'F', long, default_value = "text")] - format: Format, - - #[arg(short = 'u', long)] - /// Golem base url. Default: GOLEM_BASE_URL environment variable or http://localhost:9881. - /// - /// You can also specify different URLs for different services - /// via GOLEM_COMPONENT_BASE_URL and GOLEM_WORKER_BASE_URL - /// environment variables. - golem_url: Option, - - #[command(subcommand)] - command: Command, -} +use golem_cli::factory::ServiceFactory; +use golem_cli::oss::command::{GolemOssCommand, OssCommand}; +use golem_cli::oss::factory::OssServiceFactory; +use golem_cli::oss::model::OssContext; +use golem_cli::stubgen::handle_stubgen; fn main() -> Result<(), Box> { - let command = GolemCommand::parse(); + let command = GolemOssCommand::parse(); if let Some(level) = command.verbosity.log_level() { let tracing_level = match level { @@ -159,7 +54,7 @@ fn main() -> Result<(), Box> { .block_on(async_main(command)) } -async fn async_main(cmd: GolemCommand) -> Result<(), Box> { +async fn async_main(cmd: GolemOssCommand) -> Result<(), Box> { let url_str = cmd .golem_url .or_else(|| std::env::var("GOLEM_BASE_URL").ok()) @@ -175,150 +70,61 @@ async fn async_main(cmd: GolemCommand) -> Result<(), Box> let allow_insecure_str = std::env::var("GOLEM_ALLOW_INSECURE").unwrap_or("false".to_string()); let allow_insecure = allow_insecure_str != "false"; - let mut builder = reqwest::Client::builder(); - if allow_insecure { - builder = builder.danger_accept_invalid_certs(true); - } - let client = builder.connection_verbose(true).build()?; - - let component_context = Context { - base_url: component_url.clone(), - client: client.clone(), - }; - - let worker_context = Context { - base_url: worker_url.clone(), - client: client.clone(), - }; - - let component_client = ComponentClientLive { - client: golem_client::api::ComponentClientLive { - context: component_context.clone(), - }, - }; - let component_srv = ComponentHandlerLive { - client: component_client, - }; - let worker_client = WorkerClientLive { - client: golem_client::api::WorkerClientLive { - context: worker_context.clone(), - }, - context: worker_context.clone(), + let factory = OssServiceFactory { + component_url, + worker_url, allow_insecure, }; - let worker_srv = WorkerHandlerLive { - client: worker_client, - components: &component_srv, - worker_context: worker_context.clone(), - component_context: component_context.clone(), - allow_insecure, - }; - - let api_definition_client = ApiDefinitionClientLive { - client: golem_client::api::ApiDefinitionClientLive { - context: worker_context.clone(), - }, - }; - let api_definition_srv = ApiDefinitionHandlerLive { - client: api_definition_client, - }; - - let api_deployment_client = ApiDeploymentClientLive { - client: golem_client::api::ApiDeploymentClientLive { - context: worker_context.clone(), - }, - }; - - let api_deployment_srv = ApiDeploymentHandlerLive { - client: api_deployment_client, - }; - - let health_check_client_for_component = HealthCheckClientLive { - client: golem_client::api::HealthCheckClientLive { - context: component_context.clone(), - }, - }; - - let health_check_client_for_worker = HealthCheckClientLive { - client: golem_client::api::HealthCheckClientLive { - context: worker_context.clone(), - }, - }; - - let update_srv = VersionHandlerLive { - component_client: health_check_client_for_component, - worker_client: health_check_client_for_worker, - }; + let ctx = &OssContext::EMPTY; let yellow = "\x1b[33m"; let reset_color = "\x1b[0m"; - let version_check = update_srv.check().await; + let version_check = factory.version_service(ctx)?.check().await; if let Err(err) = version_check { eprintln!("{}{}{}", yellow, err.0, reset_color) } let res = match cmd.command { - Command::Component { subcommand } => component_srv.handle(subcommand).await, - Command::Worker { subcommand } => worker_srv.handle(cmd.format, subcommand).await, - Command::New { + OssCommand::Component { subcommand } => { + subcommand + .handle(factory.component_service(ctx)?.as_ref()) + .await + } + OssCommand::Worker { subcommand } => { + subcommand + .handle(cmd.format, factory.worker_service(ctx)?.as_ref()) + .await + } + OssCommand::New { example, package_name, component_name, } => examples::process_new(example, component_name, package_name), - Command::ListExamples { min_tier, language } => { + OssCommand::ListExamples { min_tier, language } => { examples::process_list_examples(min_tier, language) } #[cfg(feature = "stubgen")] - Command::Stubgen { subcommand } => match subcommand { - golem_wasm_rpc_stubgen::Command::Generate(args) => { - golem_wasm_rpc_stubgen::generate(args) - .map_err(|err| GolemError(format!("{err}"))) - .map(|_| GolemResult::Str("Done".to_string())) - } - golem_wasm_rpc_stubgen::Command::Build(args) => golem_wasm_rpc_stubgen::build(args) + OssCommand::Stubgen { subcommand } => handle_stubgen(subcommand).await, + OssCommand::ApiDefinition { subcommand } => { + subcommand + .handle(factory.api_definition_service(ctx)?.as_ref()) .await - .map_err(|err| GolemError(format!("{err}"))) - .map(|_| GolemResult::Str("Done".to_string())), - golem_wasm_rpc_stubgen::Command::AddStubDependency(args) => { - golem_wasm_rpc_stubgen::add_stub_dependency(args) - .map_err(|err| GolemError(format!("{err}"))) - .map(|_| GolemResult::Str("Done".to_string())) - } - golem_wasm_rpc_stubgen::Command::Compose(args) => golem_wasm_rpc_stubgen::compose(args) - .map_err(|err| GolemError(format!("{err}"))) - .map(|_| GolemResult::Str("Done".to_string())), - golem_wasm_rpc_stubgen::Command::InitializeWorkspace(args) => { - golem_wasm_rpc_stubgen::initialize_workspace(args, "golem-cli", &["stubgen"]) - .map_err(|err| GolemError(format!("{err}"))) - .map(|_| GolemResult::Str("Done".to_string())) - } - }, - Command::ApiDefinition { subcommand } => api_definition_srv.handle(subcommand).await, - Command::ApiDeployment { subcommand } => api_deployment_srv.handle(subcommand).await, + } + OssCommand::ApiDeployment { subcommand } => { + subcommand + .handle(factory.api_deployment_service(ctx)?.as_ref()) + .await + } }; match res { - Ok(res) => match res { - GolemResult::Ok(r) => { - r.println(&cmd.format); - - Ok(()) - } - GolemResult::Str(s) => { - println!("{s}"); - - Ok(()) - } - GolemResult::Json(json) => match &cmd.format { - Format::Json | Format::Text => { - Ok(println!("{}", serde_json::to_string_pretty(&json).unwrap())) - } - Format::Yaml => Ok(println!("{}", serde_yaml::to_string(&json).unwrap())), - }, - }, + Ok(res) => { + res.print(cmd.format); + Ok(()) + } Err(err) => Err(Box::new(err)), } } diff --git a/golem-cli/src/model.rs b/golem-cli/src/model.rs index e2353460a..ef8543d24 100644 --- a/golem-cli/src/model.rs +++ b/golem-cli/src/model.rs @@ -17,16 +17,20 @@ pub mod invoke_result_view; pub mod text; pub mod wave; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; use std::str::FromStr; +use crate::cloud::model::AccountId; use crate::model::text::TextFormat; use clap::builder::{StringValueParser, TypedValueParser}; use clap::error::{ContextKind, ContextValue, ErrorKind}; use clap::{Arg, ArgMatches, Command, Error, FromArgMatches}; use derive_more::{Display, FromStr}; +use golem_client::model::{ApiSite, ScanCursor}; use golem_examples::model::{Example, ExampleName, GuestLanguage, GuestLanguageTier}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; @@ -43,6 +47,19 @@ impl GolemResult { pub fn err(s: String) -> Result { Err(GolemError(s)) } + + pub fn print(self, format: Format) { + match self { + GolemResult::Ok(r) => r.println(&format), + GolemResult::Str(s) => println!("{s}"), + GolemResult::Json(json) => match format { + Format::Json | Format::Text => { + println!("{}", serde_json::to_string_pretty(&json).unwrap()) + } + Format::Yaml => println!("{}", serde_yaml::to_string(&json).unwrap()), + }, + } + } } pub trait PrintRes { @@ -78,9 +95,11 @@ impl From for GolemError { } } -impl From> - for GolemError -{ +pub trait ResponseContentErrorMapper { + fn map(self) -> String; +} + +impl From> for GolemError { fn from(value: golem_client::Error) -> Self { match value { golem_client::Error::Reqwest(error) => GolemError::from(error), @@ -89,7 +108,7 @@ impl From { - let error_str = crate::clients::errors::ResponseContentErrorMapper::map(data); + let error_str = ResponseContentErrorMapper::map(data); GolemError(error_str) } golem_client::Error::Unexpected { code, data } => { @@ -106,6 +125,62 @@ impl From From> for GolemError { + fn from(value: golem_cloud_client::Error) -> Self { + match value { + golem_cloud_client::Error::Reqwest(error) => GolemError::from(error), + golem_cloud_client::Error::ReqwestHeader(invalid_header) => { + GolemError::from(invalid_header) + } + golem_cloud_client::Error::Serde(error) => { + GolemError(format!("Unexpected serialization error: {error}")) + } + golem_cloud_client::Error::Item(data) => { + let error_str = ResponseContentErrorMapper::map(data); + GolemError(error_str) + } + golem_cloud_client::Error::Unexpected { code, data } => { + match String::from_utf8(Vec::from(data)) { + Ok(data_string) => GolemError(format!( + "Unexpected http error. Code: {code}, content: {data_string}." + )), + Err(_) => GolemError(format!( + "Unexpected http error. Code: {code}, can't parse content as string." + )), + } + } + } + } +} + +impl From> for GolemError { + fn from(value: golem_cloud_worker_client::Error) -> Self { + match value { + golem_cloud_worker_client::Error::Reqwest(error) => GolemError::from(error), + golem_cloud_worker_client::Error::ReqwestHeader(invalid_header) => { + GolemError::from(invalid_header) + } + golem_cloud_worker_client::Error::Serde(error) => { + GolemError(format!("Unexpected serialization error: {error}")) + } + golem_cloud_worker_client::Error::Item(data) => { + let error_str = ResponseContentErrorMapper::map(data); + GolemError(error_str) + } + golem_cloud_worker_client::Error::Unexpected { code, data } => { + match String::from_utf8(Vec::from(data)) { + Ok(data_string) => GolemError(format!( + "Unexpected http error. Code: {code}, content: {data_string}." + )), + Err(_) => GolemError(format!( + "Unexpected http error. Code: {code}, can't parse content as string." + )), + } + } + } + } +} + impl Display for GolemError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let GolemError(s) = self; @@ -347,3 +422,243 @@ impl FromStr for WorkerUpdateMode { } } } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WorkerMetadata { + #[serde(rename = "workerId")] + pub worker_id: golem_client::model::WorkerId, + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub account_id: Option, + pub args: Vec, + pub env: HashMap, + pub status: golem_client::model::WorkerStatus, + #[serde(rename = "componentVersion")] + pub component_version: u64, + #[serde(rename = "retryCount")] + pub retry_count: u64, + #[serde(rename = "pendingInvocationCount")] + pub pending_invocation_count: u64, + pub updates: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "lastError")] + pub last_error: Option, +} + +impl From for WorkerMetadata { + fn from(value: golem_client::model::WorkerMetadata) -> Self { + let golem_client::model::WorkerMetadata { + worker_id, + args, + env, + status, + component_version, + retry_count, + pending_invocation_count, + updates, + created_at, + last_error, + } = value; + + WorkerMetadata { + worker_id, + account_id: None, + args, + env, + status, + component_version, + retry_count, + pending_invocation_count, + updates, + created_at, + last_error, + } + } +} + +pub fn to_oss_worker_id( + id: golem_cloud_worker_client::model::WorkerId, +) -> golem_client::model::WorkerId { + golem_client::model::WorkerId { + component_id: id.component_id, + worker_name: id.worker_name, + } +} + +pub fn to_oss_worker_status( + s: golem_cloud_worker_client::model::WorkerStatus, +) -> golem_client::model::WorkerStatus { + match s { + golem_cloud_worker_client::model::WorkerStatus::Running => { + golem_client::model::WorkerStatus::Running + } + golem_cloud_worker_client::model::WorkerStatus::Idle => { + golem_client::model::WorkerStatus::Idle + } + golem_cloud_worker_client::model::WorkerStatus::Suspended => { + golem_client::model::WorkerStatus::Suspended + } + golem_cloud_worker_client::model::WorkerStatus::Interrupted => { + golem_client::model::WorkerStatus::Interrupted + } + golem_cloud_worker_client::model::WorkerStatus::Retrying => { + golem_client::model::WorkerStatus::Retrying + } + golem_cloud_worker_client::model::WorkerStatus::Failed => { + golem_client::model::WorkerStatus::Failed + } + golem_cloud_worker_client::model::WorkerStatus::Exited => { + golem_client::model::WorkerStatus::Exited + } + } +} + +fn to_oss_update_record( + r: golem_cloud_worker_client::model::UpdateRecord, +) -> golem_client::model::UpdateRecord { + fn to_oss_pending_update( + u: golem_cloud_worker_client::model::PendingUpdate, + ) -> golem_client::model::PendingUpdate { + golem_client::model::PendingUpdate { + timestamp: u.timestamp, + target_version: u.target_version, + } + } + fn to_oss_successful_update( + u: golem_cloud_worker_client::model::SuccessfulUpdate, + ) -> golem_client::model::SuccessfulUpdate { + golem_client::model::SuccessfulUpdate { + timestamp: u.timestamp, + target_version: u.target_version, + } + } + fn to_oss_failed_update( + u: golem_cloud_worker_client::model::FailedUpdate, + ) -> golem_client::model::FailedUpdate { + golem_client::model::FailedUpdate { + timestamp: u.timestamp, + target_version: u.target_version, + details: u.details, + } + } + + match r { + golem_cloud_worker_client::model::UpdateRecord::PendingUpdate(pu) => { + golem_client::model::UpdateRecord::PendingUpdate(to_oss_pending_update(pu)) + } + golem_cloud_worker_client::model::UpdateRecord::SuccessfulUpdate(su) => { + golem_client::model::UpdateRecord::SuccessfulUpdate(to_oss_successful_update(su)) + } + golem_cloud_worker_client::model::UpdateRecord::FailedUpdate(fu) => { + golem_client::model::UpdateRecord::FailedUpdate(to_oss_failed_update(fu)) + } + } +} + +impl From for WorkerMetadata { + fn from(value: golem_cloud_worker_client::model::WorkerMetadata) -> Self { + let golem_cloud_worker_client::model::WorkerMetadata { + worker_id, + account_id, + args, + env, + status, + component_version, + retry_count, + pending_invocation_count, + updates, + created_at, + last_error, + } = value; + + WorkerMetadata { + worker_id: to_oss_worker_id(worker_id), + account_id: Some(AccountId::new(account_id)), + args, + env, + status: to_oss_worker_status(status), + component_version, + retry_count, + pending_invocation_count, + updates: updates.into_iter().map(to_oss_update_record).collect(), + created_at, + last_error, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WorkersMetadataResponse { + pub workers: Vec, + pub cursor: Option, +} + +impl From for WorkersMetadataResponse { + fn from(value: golem_client::model::WorkersMetadataResponse) -> Self { + WorkersMetadataResponse { + cursor: value.cursor, + workers: value.workers.into_iter().map(|m| m.into()).collect(), + } + } +} + +impl From for WorkersMetadataResponse { + fn from(value: golem_cloud_worker_client::model::WorkersMetadataResponse) -> Self { + WorkersMetadataResponse { + cursor: value.cursor.map(|c| golem_client::model::ScanCursor{ cursor: c, layer: 0 }), // TODO: unify cloud and OSS + workers: value.workers.into_iter().map(|m| m.into()).collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ApiDeployment { + #[serde(rename = "apiDefinitionId")] + pub api_definition_id: String, + pub version: String, + #[serde(rename = "projectId")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub project_id: Option, + pub site: ApiSite, +} + +impl From for ApiDeployment { + fn from(value: golem_client::model::ApiDeployment) -> Self { + let golem_client::model::ApiDeployment { + api_definition_id, + version, + site, + } = value; + + ApiDeployment { + api_definition_id, + version, + project_id: None, + site, + } + } +} + +impl From for ApiDeployment { + fn from(value: golem_cloud_worker_client::model::ApiDeployment) -> Self { + let golem_cloud_worker_client::model::ApiDeployment { + api_definition_id, + version, + project_id, + site: golem_cloud_worker_client::model::ApiSite { host, subdomain }, + } = value; + + ApiDeployment { + api_definition_id, + version, + project_id: Some(project_id), + site: ApiSite { + host, + subdomain: Some(subdomain), + }, + } + } +} diff --git a/golem-cli/src/model/component.rs b/golem-cli/src/model/component.rs index cafa42526..858a0e801 100644 --- a/golem-cli/src/model/component.rs +++ b/golem-cli/src/model/component.rs @@ -1,7 +1,10 @@ +use crate::cloud::model::ProjectId; use crate::model::GolemError; use golem_client::model::{ - Component, Export, ExportFunction, ExportInstance, FunctionResult, NameOptionTypePair, - NameTypePair, ResourceMode, Type, TypeEnum, TypeFlags, TypeRecord, TypeTuple, TypeVariant, + ComponentMetadata, Export, ExportFunction, ExportInstance, FunctionParameter, FunctionResult, + NameOptionTypePair, NameTypePair, ProducerField, Producers, ProtectedComponentId, ResourceMode, + Type, TypeEnum, TypeFlags, TypeHandle, TypeList, TypeOption, TypeRecord, TypeResult, TypeTuple, + TypeVariant, UserComponentId, VersionedComponentId, VersionedName, }; use golem_wasm_ast::wave::DisplayNamedFunc; use serde::{Deserialize, Serialize}; @@ -9,6 +12,294 @@ use tracing::info; use crate::model::wave::{func_to_analysed, function_wave_compatible}; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Component { + pub versioned_component_id: VersionedComponentId, + pub user_component_id: UserComponentId, + pub protected_component_id: ProtectedComponentId, + pub component_name: String, + pub component_size: u64, + pub metadata: ComponentMetadata, + pub project_id: Option, +} + +impl From for Component { + fn from(value: golem_client::model::Component) -> Self { + let golem_client::model::Component { + versioned_component_id, + user_component_id, + protected_component_id, + component_name, + component_size, + metadata, + } = value; + + Component { + versioned_component_id, + user_component_id, + protected_component_id, + component_name, + component_size, + metadata, + project_id: None, + } + } +} + +impl From for Component { + fn from(value: golem_cloud_client::model::Component) -> Self { + let golem_cloud_client::model::Component { + versioned_component_id, + user_component_id, + protected_component_id, + component_name, + component_size, + metadata, + project_id, + } = value; + + Component { + versioned_component_id: to_oss_versioned_component_id(versioned_component_id), + user_component_id: to_oss_user_component_id(user_component_id), + protected_component_id: to_oss_protected_component_id(protected_component_id), + component_name, + component_size, + metadata: to_oss_metadata(metadata), + project_id: Some(ProjectId(project_id)), + } + } +} + +fn to_oss_versioned_component_id( + id: golem_cloud_client::model::VersionedComponentId, +) -> VersionedComponentId { + VersionedComponentId { + component_id: id.component_id, + version: id.version, + } +} + +fn to_oss_user_component_id(id: golem_cloud_client::model::UserComponentId) -> UserComponentId { + UserComponentId { + versioned_component_id: to_oss_versioned_component_id(id.versioned_component_id), + } +} + +fn to_oss_protected_component_id( + id: golem_cloud_client::model::ProtectedComponentId, +) -> ProtectedComponentId { + ProtectedComponentId { + versioned_component_id: to_oss_versioned_component_id(id.versioned_component_id), + } +} + +fn to_oss_metadata(metadata: golem_cloud_client::model::ComponentMetadata) -> ComponentMetadata { + let golem_cloud_client::model::ComponentMetadata { exports, producers } = metadata; + + fn to_oss_name_option_type_pair( + p: golem_cloud_client::model::NameOptionTypePair, + ) -> NameOptionTypePair { + let golem_cloud_client::model::NameOptionTypePair { name, typ } = p; + + NameOptionTypePair { + name, + typ: typ.map(to_oss_type), + } + } + + fn to_oss_type_variant(tv: golem_cloud_client::model::TypeVariant) -> TypeVariant { + TypeVariant { + cases: tv + .cases + .into_iter() + .map(to_oss_name_option_type_pair) + .collect(), + } + } + + fn to_oss_type_result(r: golem_cloud_client::model::TypeResult) -> TypeResult { + let golem_cloud_client::model::TypeResult { ok, err } = r; + + TypeResult { + ok: ok.map(to_oss_type), + err: err.map(to_oss_type), + } + } + + fn to_oss_type_option(o: golem_cloud_client::model::TypeOption) -> TypeOption { + TypeOption { + inner: to_oss_type(o.inner), + } + } + + fn to_oss_type_enum(e: golem_cloud_client::model::TypeEnum) -> TypeEnum { + TypeEnum { cases: e.cases } + } + + fn to_oss_type_flags(f: golem_cloud_client::model::TypeFlags) -> TypeFlags { + TypeFlags { cases: f.cases } + } + + fn to_oss_name_type_pair(p: golem_cloud_client::model::NameTypePair) -> NameTypePair { + let golem_cloud_client::model::NameTypePair { name, typ } = p; + + NameTypePair { + name, + typ: to_oss_type(typ), + } + } + + fn to_oss_type_record(r: golem_cloud_client::model::TypeRecord) -> TypeRecord { + TypeRecord { + cases: r.cases.into_iter().map(to_oss_name_type_pair).collect(), + } + } + + fn to_oss_type_tuple(t: golem_cloud_client::model::TypeTuple) -> TypeTuple { + TypeTuple { + items: t.items.into_iter().map(to_oss_type).collect(), + } + } + + fn to_oss_type_list(l: golem_cloud_client::model::TypeList) -> TypeList { + TypeList { + inner: to_oss_type(l.inner), + } + } + + fn to_oss_type_handle(h: golem_cloud_client::model::TypeHandle) -> TypeHandle { + let mode = match &h.mode { + golem_cloud_client::model::ResourceMode::Borrowed => ResourceMode::Borrowed, + golem_cloud_client::model::ResourceMode::Owned => ResourceMode::Owned, + }; + + TypeHandle { + resource_id: h.resource_id, + mode, + } + } + + fn to_oss_type(typ: golem_cloud_client::model::Type) -> Type { + match typ { + golem_cloud_client::model::Type::Variant(tv) => Type::Variant(to_oss_type_variant(tv)), + golem_cloud_client::model::Type::Result(r) => { + Type::Result(Box::new(to_oss_type_result(*r))) + } + golem_cloud_client::model::Type::Option(o) => { + Type::Option(Box::new(to_oss_type_option(*o))) + } + golem_cloud_client::model::Type::Enum(e) => Type::Enum(to_oss_type_enum(e)), + golem_cloud_client::model::Type::Flags(f) => Type::Flags(to_oss_type_flags(f)), + golem_cloud_client::model::Type::Record(r) => Type::Record(to_oss_type_record(r)), + golem_cloud_client::model::Type::Tuple(t) => Type::Tuple(to_oss_type_tuple(t)), + golem_cloud_client::model::Type::List(l) => Type::List(Box::new(to_oss_type_list(*l))), + golem_cloud_client::model::Type::Str(_) => Type::Str(golem_client::model::TypeStr {}), + golem_cloud_client::model::Type::Chr(_) => Type::Chr(golem_client::model::TypeChr {}), + golem_cloud_client::model::Type::F64(_) => Type::F64(golem_client::model::TypeF64 {}), + golem_cloud_client::model::Type::F32(_) => Type::F32(golem_client::model::TypeF32 {}), + golem_cloud_client::model::Type::U64(_) => Type::U64(golem_client::model::TypeU64 {}), + golem_cloud_client::model::Type::S64(_) => Type::S64(golem_client::model::TypeS64 {}), + golem_cloud_client::model::Type::U32(_) => Type::U32(golem_client::model::TypeU32 {}), + golem_cloud_client::model::Type::S32(_) => Type::S32(golem_client::model::TypeS32 {}), + golem_cloud_client::model::Type::U16(_) => Type::U16(golem_client::model::TypeU16 {}), + golem_cloud_client::model::Type::S16(_) => Type::S16(golem_client::model::TypeS16 {}), + golem_cloud_client::model::Type::U8(_) => Type::U8(golem_client::model::TypeU8 {}), + golem_cloud_client::model::Type::S8(_) => Type::S8(golem_client::model::TypeS8 {}), + golem_cloud_client::model::Type::Bool(_) => { + Type::Bool(golem_client::model::TypeBool {}) + } + golem_cloud_client::model::Type::Handle(h) => Type::Handle(to_oss_type_handle(h)), + } + } + + fn to_oss_function_parameter( + p: golem_cloud_client::model::FunctionParameter, + ) -> FunctionParameter { + let golem_cloud_client::model::FunctionParameter { name, typ } = p; + + FunctionParameter { + name, + typ: to_oss_type(typ), + } + } + + fn to_oss_function_result(r: golem_cloud_client::model::FunctionResult) -> FunctionResult { + let golem_cloud_client::model::FunctionResult { name, typ } = r; + + FunctionResult { + name, + typ: to_oss_type(typ), + } + } + + fn to_oss_export_function( + function: golem_cloud_client::model::ExportFunction, + ) -> ExportFunction { + let golem_cloud_client::model::ExportFunction { + name, + parameters, + results, + } = function; + + ExportFunction { + name, + parameters: parameters + .into_iter() + .map(to_oss_function_parameter) + .collect(), + results: results.into_iter().map(to_oss_function_result).collect(), + } + } + + fn to_oss_export_instance( + instance: golem_cloud_client::model::ExportInstance, + ) -> ExportInstance { + let golem_cloud_client::model::ExportInstance { name, functions } = instance; + + ExportInstance { + name, + functions: functions.into_iter().map(to_oss_export_function).collect(), + } + } + + fn to_oss_export(export: golem_cloud_client::model::Export) -> Export { + match export { + golem_cloud_client::model::Export::Instance(instance) => { + Export::Instance(to_oss_export_instance(instance)) + } + golem_cloud_client::model::Export::Function(function) => { + Export::Function(to_oss_export_function(function)) + } + } + } + + fn to_oss_versioned_name(n: golem_cloud_client::model::VersionedName) -> VersionedName { + let golem_cloud_client::model::VersionedName { name, version } = n; + + VersionedName { name, version } + } + + fn to_oss_producer_field(f: golem_cloud_client::model::ProducerField) -> ProducerField { + let golem_cloud_client::model::ProducerField { name, values } = f; + + ProducerField { + name, + values: values.into_iter().map(to_oss_versioned_name).collect(), + } + } + + fn to_oss_producers(p: golem_cloud_client::model::Producers) -> Producers { + Producers { + fields: p.fields.into_iter().map(to_oss_producer_field).collect(), + } + } + + ComponentMetadata { + exports: exports.into_iter().map(to_oss_export).collect(), + producers: producers.into_iter().map(to_oss_producers).collect(), + } +} + #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ComponentView { @@ -16,6 +307,9 @@ pub struct ComponentView { pub component_version: u64, pub component_name: String, pub component_size: u64, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub project_id: Option, pub exports: Vec, } @@ -32,6 +326,7 @@ impl From<&Component> for ComponentView { component_version: value.versioned_component_id.version, component_name: value.component_name.to_string(), component_size: value.component_size, + project_id: value.project_id, exports: value .metadata .exports diff --git a/golem-cli/src/model/invoke_result_view.rs b/golem-cli/src/model/invoke_result_view.rs index cad7fcc98..bec935847 100644 --- a/golem-cli/src/model/invoke_result_view.rs +++ b/golem-cli/src/model/invoke_result_view.rs @@ -1,7 +1,7 @@ -use crate::model::component::function_result_types; +use crate::model::component::{function_result_types, Component}; use crate::model::wave::{type_to_analysed, type_wave_compatible}; use crate::model::GolemError; -use golem_client::model::{Component, InvokeResult, Type}; +use golem_client::model::{InvokeResult, Type}; use golem_wasm_rpc::TypeAnnotatedValue; use serde::{Deserialize, Serialize}; use serde_json::value::Value; @@ -94,10 +94,11 @@ impl InvokeResultView { #[cfg(test)] mod tests { + use crate::model::component::Component; use crate::model::invoke_result_view::InvokeResultView; use crate::model::wave::type_to_analysed; use golem_client::model::{ - Component, ComponentMetadata, Export, ExportFunction, FunctionResult, InvokeResult, + ComponentMetadata, Export, ExportFunction, FunctionResult, InvokeResult, ProtectedComponentId, ResourceMode, Type, TypeBool, TypeHandle, UserComponentId, VersionedComponentId, }; @@ -147,6 +148,7 @@ mod tests { results: func_res, })], }, + project_id: None, }; InvokeResultView::try_parse_or_json(InvokeResult { result: json }, &component, "func_name") diff --git a/golem-cli/src/model/text.rs b/golem-cli/src/model/text.rs index 0c015d272..de6346ff6 100644 --- a/golem-cli/src/model/text.rs +++ b/golem-cli/src/model/text.rs @@ -1,11 +1,10 @@ use crate::model::component::ComponentView; use crate::model::invoke_result_view::InvokeResultView; -use crate::model::{ExampleDescription, IdempotencyKey}; -use cli_table::{format::Justify, print_stdout, Table, WithTitle}; -use golem_client::model::{ - ApiDeployment, HttpApiDefinition, Route, ScanCursor, WorkerId, WorkerMetadata, - WorkersMetadataResponse, +use crate::model::{ + ApiDeployment, ExampleDescription, IdempotencyKey, WorkerMetadata, WorkersMetadataResponse, }; +use cli_table::{format::Justify, print_stdout, Table, WithTitle}; +use golem_client::model::{HttpApiDefinition, Route, ScanCursor, WorkerId}; use golem_examples::model::{ExampleName, GuestLanguage, GuestLanguageTier}; use indoc::{eprintdoc, printdoc}; use itertools::Itertools; diff --git a/golem-cli/src/oss.rs b/golem-cli/src/oss.rs new file mode 100644 index 000000000..d11e76590 --- /dev/null +++ b/golem-cli/src/oss.rs @@ -0,0 +1,18 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod clients; +pub mod command; +pub mod factory; +pub mod model; diff --git a/golem-cli/src/oss/clients.rs b/golem-cli/src/oss/clients.rs new file mode 100644 index 000000000..b874f553e --- /dev/null +++ b/golem-cli/src/oss/clients.rs @@ -0,0 +1,20 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod api_definition; +pub mod api_deployment; +pub mod component; +pub mod errors; +pub mod health_check; +pub mod worker; diff --git a/golem-cli/src/oss/clients/api_definition.rs b/golem-cli/src/oss/clients/api_definition.rs new file mode 100644 index 000000000..4fcc02315 --- /dev/null +++ b/golem-cli/src/oss/clients/api_definition.rs @@ -0,0 +1,170 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Display; + +use std::io::Read; + +use async_trait::async_trait; + +use golem_client::model::HttpApiDefinition; + +use crate::clients::api_definition::ApiDefinitionClient; +use tokio::fs::read_to_string; +use tracing::info; + +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, PathBufOrStdin}; +use crate::oss::model::OssContext; + +#[derive(Clone)] +pub struct ApiDefinitionClientLive { + pub client: C, +} + +#[derive(Debug, Copy, Clone)] +enum Action { + Create, + Update, + Import, +} + +impl Display for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Action::Create => "Creating", + Action::Update => "Updating", + Action::Import => "Importing", + }; + write!(f, "{}", str) + } +} + +async fn create_or_update_api_definition< + C: golem_client::api::ApiDefinitionClient + Sync + Send, +>( + action: Action, + client: &C, + path: PathBufOrStdin, +) -> Result { + info!("{action} api definition from {path:?}"); + + let definition_str: String = match path { + PathBufOrStdin::Path(path) => read_to_string(path) + .await + .map_err(|e| GolemError(format!("Failed to read from file: {e:?}")))?, + PathBufOrStdin::Stdin => { + let mut content = String::new(); + + let _ = std::io::stdin() + .read_to_string(&mut content) + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + content + } + }; + + match action { + Action::Import => { + let value: serde_json::value::Value = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse json: {e:?}")))?; + + Ok(client.import_open_api(&value).await?) + } + Action::Create => { + let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; + + Ok(client.create_definition(&value).await?) + } + Action::Update => { + let value: HttpApiDefinition = serde_json::from_str(definition_str.as_str()) + .map_err(|e| GolemError(format!("Failed to parse HttpApiDefinition: {e:?}")))?; + + Ok(client + .update_definition(&value.id, &value.version, &value) + .await?) + } + } +} + +#[async_trait] +impl ApiDefinitionClient + for ApiDefinitionClientLive +{ + type ProjectContext = OssContext; + + async fn list( + &self, + id: Option<&ApiDefinitionId>, + _project: &Self::ProjectContext, + ) -> Result, GolemError> { + info!("Getting api definitions"); + + Ok(self + .client + .list_definitions(id.map(|id| id.0.as_str())) + .await?) + } + + async fn get( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + _project: &Self::ProjectContext, + ) -> Result { + info!("Getting api definition for {}/{}", id.0, version.0); + + Ok(self + .client + .get_definition(id.0.as_str(), version.0.as_str()) + .await?) + } + + async fn create( + &self, + path: PathBufOrStdin, + _project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Create, &self.client, path).await + } + + async fn update( + &self, + path: PathBufOrStdin, + _project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Update, &self.client, path).await + } + + async fn import( + &self, + path: PathBufOrStdin, + _project: &Self::ProjectContext, + ) -> Result { + create_or_update_api_definition(Action::Import, &self.client, path).await + } + + async fn delete( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + _project: &Self::ProjectContext, + ) -> Result { + info!("Deleting api definition for {}/{}", id.0, version.0); + Ok(self + .client + .delete_definition(id.0.as_str(), version.0.as_str()) + .await?) + } +} diff --git a/golem-cli/src/oss/clients/api_deployment.rs b/golem-cli/src/oss/clients/api_deployment.rs new file mode 100644 index 000000000..27c22e5e3 --- /dev/null +++ b/golem-cli/src/oss/clients/api_deployment.rs @@ -0,0 +1,86 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use itertools::Itertools; + +use crate::clients::api_deployment::ApiDeploymentClient; +use golem_client::model::ApiSite; +use tracing::info; + +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, ApiDeployment, GolemError}; +use crate::oss::model::OssContext; + +#[derive(Clone)] +pub struct ApiDeploymentClientLive { + pub client: C, +} + +#[async_trait] +impl ApiDeploymentClient + for ApiDeploymentClientLive +{ + type ProjectContext = OssContext; + + async fn deploy( + &self, + api_definition_id: &ApiDefinitionId, + version: &ApiDefinitionVersion, + host: &str, + subdomain: Option, + _project: &Self::ProjectContext, + ) -> Result { + info!( + "Deploying definition {api_definition_id}/{version}, host {host} {}", + subdomain + .clone() + .map_or("".to_string(), |s| format!("subdomain {}", s)) + ); + + let deployment = golem_client::model::ApiDeployment { + api_definition_id: api_definition_id.0.to_string(), + version: version.0.to_string(), + site: ApiSite { + host: host.to_string(), + subdomain, + }, + }; + + Ok(self.client.deploy(&deployment).await?.into()) + } + + async fn list( + &self, + api_definition_id: &ApiDefinitionId, + _project: &Self::ProjectContext, + ) -> Result, GolemError> { + info!("List api deployments with definition {api_definition_id}"); + + let deployments = self.client.list_deployments(&api_definition_id.0).await?; + + Ok(deployments.into_iter().map_into().collect()) + } + + async fn get(&self, site: &str) -> Result { + info!("Getting api deployment for site {site}"); + + Ok(self.client.get_deployment(site).await?.into()) + } + + async fn delete(&self, site: &str) -> Result { + info!("Deleting api deployment for site {site}"); + + Ok(self.client.delete_deployment(site).await?) + } +} diff --git a/golem-cli/src/oss/clients/component.rs b/golem-cli/src/oss/clients/component.rs new file mode 100644 index 000000000..c4808d706 --- /dev/null +++ b/golem-cli/src/oss/clients/component.rs @@ -0,0 +1,132 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Read; + +use async_trait::async_trait; + +use crate::clients::component::ComponentClient; +use tokio::fs::File; +use tracing::info; + +use crate::model::component::Component; +use crate::model::{ComponentId, ComponentName, GolemError, PathBufOrStdin}; +use crate::oss::model::OssContext; + +#[derive(Debug, Clone)] +pub struct ComponentClientLive { + pub client: C, +} + +#[async_trait] +impl ComponentClient + for ComponentClientLive +{ + type ProjectContext = OssContext; + + async fn get_metadata( + &self, + component_id: &ComponentId, + version: u64, + ) -> Result { + info!("Getting component version"); + + Ok(self + .client + .get_component_metadata(&component_id.0, &version.to_string()) + .await? + .into()) + } + + async fn get_latest_metadata( + &self, + component_id: &ComponentId, + ) -> Result { + info!("Getting latest component version"); + + Ok(self + .client + .get_latest_component_metadata(&component_id.0) + .await? + .into()) + } + + async fn find( + &self, + name: Option, + _project: &Option, + ) -> Result, GolemError> { + info!("Getting components"); + + let name = name.map(|n| n.0); + + let components = self.client.get_components(name.as_deref()).await?; + Ok(components.into_iter().map(|c| c.into()).collect()) + } + + async fn add( + &self, + name: ComponentName, + path: PathBufOrStdin, + _project: &Option, + ) -> Result { + info!("Adding component {name:?} from {path:?}"); + + let component = match path { + PathBufOrStdin::Path(path) => { + let file = File::open(path) + .await + .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; + + self.client.create_component(&name.0, file).await? + } + PathBufOrStdin::Stdin => { + let mut bytes = Vec::new(); + + let _ = std::io::stdin() + .read_to_end(&mut bytes) // TODO: steaming request from stdin + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + self.client.create_component(&name.0, bytes).await? + } + }; + + Ok(component.into()) + } + + async fn update(&self, id: ComponentId, path: PathBufOrStdin) -> Result { + info!("Updating component {id:?} from {path:?}"); + + let component = match path { + PathBufOrStdin::Path(path) => { + let file = File::open(path) + .await + .map_err(|e| GolemError(format!("Can't open component file: {e}")))?; + + self.client.update_component(&id.0, file).await? + } + PathBufOrStdin::Stdin => { + let mut bytes = Vec::new(); + + let _ = std::io::stdin() + .read_to_end(&mut bytes) + .map_err(|e| GolemError(format!("Failed to read stdin: {e:?}")))?; + + self.client.update_component(&id.0, bytes).await? + } + }; + + Ok(component.into()) + } +} diff --git a/golem-cli/src/clients/errors.rs b/golem-cli/src/oss/clients/errors.rs similarity index 98% rename from golem-cli/src/clients/errors.rs rename to golem-cli/src/oss/clients/errors.rs index 33bb1138c..20c842e6a 100644 --- a/golem-cli/src/clients/errors.rs +++ b/golem-cli/src/oss/clients/errors.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::model::ResponseContentErrorMapper; use golem_client::api::{ ApiDefinitionError, ApiDeploymentError, ComponentError, HealthCheckError, WorkerError, }; @@ -26,10 +27,6 @@ use golem_client::model::{ }; use itertools::Itertools; -pub trait ResponseContentErrorMapper { - fn map(self) -> String; -} - impl ResponseContentErrorMapper for ComponentError { fn map(self) -> String { match self { @@ -232,7 +229,7 @@ mod tests { }; use uuid::Uuid; - use crate::clients::errors::ResponseContentErrorMapper; + use crate::oss::clients::errors::ResponseContentErrorMapper; #[test] fn api_definition_error_409() { diff --git a/golem-cli/src/oss/clients/health_check.rs b/golem-cli/src/oss/clients/health_check.rs new file mode 100644 index 000000000..30e1e6459 --- /dev/null +++ b/golem-cli/src/oss/clients/health_check.rs @@ -0,0 +1,36 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::health_check::HealthCheckClient; +use async_trait::async_trait; +use golem_client::model::VersionInfo; +use tracing::debug; + +use crate::model::GolemError; + +#[derive(Clone)] +pub struct HealthCheckClientLive { + pub client: C, +} + +#[async_trait] +impl HealthCheckClient + for HealthCheckClientLive +{ + async fn version(&self) -> Result { + debug!("Getting server version"); + + Ok(self.client.version().await?) + } +} diff --git a/golem-cli/src/oss/clients/worker.rs b/golem-cli/src/oss/clients/worker.rs new file mode 100644 index 000000000..873b777b2 --- /dev/null +++ b/golem-cli/src/oss/clients/worker.rs @@ -0,0 +1,441 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Duration; + +use crate::clients::worker::WorkerClient; +use async_trait::async_trait; +use futures_util::{future, pin_mut, SinkExt, StreamExt}; +use golem_client::model::{CallingConvention, InvokeParameters, InvokeResult, ScanCursor, UpdateWorkerRequest, WorkerCreationRequest, WorkerFilter, WorkerId, WorkersMetadataRequest}; +use golem_client::Context; +use native_tls::TlsConnector; +use serde::Deserialize; +use tokio::{task, time}; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::protocol::Message; +use tokio_tungstenite::{connect_async_tls_with_config, Connector}; +use tracing::{debug, info}; + +use crate::model::{ + ComponentId, GolemError, IdempotencyKey, WorkerMetadata, WorkerName, WorkerUpdateMode, + WorkersMetadataResponse, +}; + +#[derive(Clone)] +pub struct WorkerClientLive { + pub client: C, + pub context: Context, + pub allow_insecure: bool, +} + +#[async_trait] +impl WorkerClient for WorkerClientLive { + async fn new_worker( + &self, + name: WorkerName, + component_id: ComponentId, + args: Vec, + env: Vec<(String, String)>, + ) -> Result { + info!("Creating worker {name} of {}", component_id.0); + + Ok(self + .client + .launch_new_worker( + &component_id.0, + &WorkerCreationRequest { + name: name.0, + args, + env: env.into_iter().collect(), + }, + ) + .await? + .worker_id) + } + + async fn invoke_and_await( + &self, + name: WorkerName, + component_id: ComponentId, + function: String, + parameters: InvokeParameters, + idempotency_key: Option, + use_stdio: bool, + ) -> Result { + info!( + "Invoke and await for function {function} in {}/{}", + component_id.0, name.0 + ); + + let calling_convention = if use_stdio { + CallingConvention::Stdio + } else { + CallingConvention::Component + }; + + Ok(self + .client + .invoke_and_await_function( + &component_id.0, + &name.0, + idempotency_key.as_ref().map(|k| k.0.as_str()), + &function, + Some(&calling_convention), + ¶meters, + ) + .await?) + } + + async fn invoke( + &self, + name: WorkerName, + component_id: ComponentId, + function: String, + parameters: InvokeParameters, + idempotency_key: Option, + ) -> Result<(), GolemError> { + info!( + "Invoke function {function} in {}/{}", + component_id.0, name.0 + ); + + let _ = self + .client + .invoke_function( + &component_id.0, + &name.0, + idempotency_key.as_ref().map(|k| k.0.as_str()), + &function, + ¶meters, + ) + .await?; + Ok(()) + } + + async fn interrupt( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result<(), GolemError> { + info!("Interrupting {}/{}", component_id.0, name.0); + + let _ = self + .client + .interrupt_worker(&component_id.0, &name.0, Some(false)) + .await?; + Ok(()) + } + + async fn simulated_crash( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result<(), GolemError> { + info!("Simulating crash of {}/{}", component_id.0, name.0); + + let _ = self + .client + .interrupt_worker(&component_id.0, &name.0, Some(true)) + .await?; + Ok(()) + } + + async fn delete(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { + info!("Deleting worker {}/{}", component_id.0, name.0); + + let _ = self.client.delete_worker(&component_id.0, &name.0).await?; + Ok(()) + } + + async fn get_metadata( + &self, + name: WorkerName, + component_id: ComponentId, + ) -> Result { + info!("Getting worker {}/{} metadata", component_id.0, name.0); + + Ok(self + .client + .get_worker_metadata(&component_id.0, &name.0) + .await? + .into()) + } + + async fn find_metadata( + &self, + component_id: ComponentId, + filter: Option, + cursor: Option, + count: Option, + precise: Option, + ) -> Result { + info!( + "Getting workers metadata for component: {}, filter: {}", + component_id.0, + filter.is_some() + ); + + Ok(self + .client + .find_workers_metadata( + &component_id.0, + &WorkersMetadataRequest { + filter, + cursor, + count, + precise, + }, + ) + .await? + .into()) + } + + async fn list_metadata( + &self, + component_id: ComponentId, + filter: Option>, + cursor: Option, + count: Option, + precise: Option, + ) -> Result { + info!( + "Getting workers metadata for component: {}, filter: {}", + component_id.0, + filter + .clone() + .map(|fs| fs.join(" AND ")) + .unwrap_or("N/A".to_string()) + ); + + let filter: Option<&[String]> = filter.as_deref(); + + let cursor = cursor.map(|cursor| format!("{}/{}", cursor.layer, cursor.cursor)); + + Ok(self + .client + .get_workers_metadata(&component_id.0, filter, cursor.as_deref(), count, precise) + .await? + .into()) + } + + async fn connect(&self, name: WorkerName, component_id: ComponentId) -> Result<(), GolemError> { + let mut url = self.context.base_url.clone(); + + let ws_schema = if url.scheme() == "http" { "ws" } else { "wss" }; + + url.set_scheme(ws_schema) + .map_err(|_| GolemError("Can't set schema.".to_string()))?; + + url.path_segments_mut() + .map_err(|_| GolemError("Can't get path.".to_string()))? + .push("v2") + .push("components") + .push(&component_id.0.to_string()) + .push("workers") + .push(&name.0) + .push("connect"); + + let mut request = url + .into_client_request() + .map_err(|e| GolemError(format!("Can't create request: {e}")))?; + let headers = request.headers_mut(); + + if let Some(token) = self.context.bearer_token() { + headers.insert( + "Authorization", + format!("Bearer {}", token).parse().unwrap(), + ); + } + + let connector = if self.allow_insecure { + Some(Connector::NativeTls( + TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(), + )) + } else { + None + }; + + let (ws_stream, _) = connect_async_tls_with_config(request, None, false, connector) + .await + .map_err(|e| match e { + tungstenite::error::Error::Http(http_error_response) => { + match http_error_response.body().clone() { + Some(body) => GolemError(format!( + "Failed Websocket. Http error: {}, {}", + http_error_response.status(), + String::from_utf8_lossy(&body) + )), + None => GolemError(format!( + "Failed Websocket. Http error: {}", + http_error_response.status() + )), + } + } + _ => GolemError(format!("Failed Websocket. Error: {}", e)), + })?; + + let (mut write, read) = ws_stream.split(); + + let pings = task::spawn(async move { + let mut interval = time::interval(Duration::from_secs(5)); // TODO configure + + let mut cnt: i32 = 1; + + loop { + interval.tick().await; + + write + .send(Message::Ping(cnt.to_ne_bytes().to_vec())) + .await + .unwrap(); // TODO: handle errors: map_err(|e| GolemError(format!("Ping failure: {e}")))?; + + cnt += 1; + } + }); + + let read_res = read.for_each(|message_or_error| async { + match message_or_error { + Err(error) => { + print!("Error reading message: {}", error); + } + Ok(message) => { + let instance_connect_msg = match message { + Message::Text(str) => { + let parsed: serde_json::Result = + serde_json::from_str(&str); + Some(parsed.unwrap()) // TODO: error handling + } + Message::Binary(data) => { + let parsed: serde_json::Result = + serde_json::from_slice(&data); + Some(parsed.unwrap()) // TODO: error handling + } + Message::Ping(_) => { + debug!("Ignore ping"); + None + } + Message::Pong(_) => { + debug!("Ignore pong"); + None + } + Message::Close(details) => { + match details { + Some(closed_frame) => { + print!("Connection Closed: {}", closed_frame); + } + None => { + print!("Connection Closed"); + } + } + None + } + Message::Frame(_) => { + info!("Ignore unexpected frame"); + None + } + }; + + match instance_connect_msg { + None => {} + Some(msg) => match msg.event { + WorkerEvent::Stdout(StdOutLog { message }) => { + print!("{message}") + } + WorkerEvent::Stderr(StdErrLog { message }) => { + print!("{message}") + } + WorkerEvent::Log(Log { + level, + context, + message, + }) => match level { + 0 => tracing::trace!(message, context = context), + 1 => tracing::debug!(message, context = context), + 2 => tracing::info!(message, context = context), + 3 => tracing::warn!(message, context = context), + _ => tracing::error!(message, context = context), + }, + }, + } + } + } + }); + + pin_mut!(read_res, pings); + + future::select(pings, read_res).await; + + Ok(()) + } + + async fn update( + &self, + name: WorkerName, + component_id: ComponentId, + mode: WorkerUpdateMode, + target_version: u64, + ) -> Result<(), GolemError> { + info!("Updating worker {name} of {}", component_id.0); + let update_mode = match mode { + WorkerUpdateMode::Automatic => golem_client::model::WorkerUpdateMode::Automatic, + WorkerUpdateMode::Manual => golem_client::model::WorkerUpdateMode::Manual, + }; + + let _ = self + .client + .update_worker( + &component_id.0, + &name.0, + &UpdateWorkerRequest { + mode: update_mode, + target_version, + }, + ) + .await?; + Ok(()) + } +} + +#[derive(Deserialize, Debug)] +struct InstanceConnectMessage { + pub event: WorkerEvent, +} + +#[derive(Deserialize, Debug)] +enum WorkerEvent { + Stdout(StdOutLog), + Stderr(StdErrLog), + Log(Log), +} + +#[derive(Deserialize, Debug)] +struct StdOutLog { + message: String, +} + +#[derive(Deserialize, Debug)] +struct StdErrLog { + message: String, +} + +#[derive(Deserialize, Debug)] +struct Log { + pub level: i32, + pub context: String, + pub message: String, +} diff --git a/golem-cli/src/oss/command.rs b/golem-cli/src/oss/command.rs new file mode 100644 index 000000000..03c1c0940 --- /dev/null +++ b/golem-cli/src/oss/command.rs @@ -0,0 +1,118 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::Format; +use crate::oss::command::api_definition::ApiDefinitionSubcommand; +use crate::oss::command::api_deployment::ApiDeploymentSubcommand; +use crate::oss::command::component::ComponentSubCommand; +use crate::oss::command::worker::WorkerSubcommand; +use clap::{Parser, Subcommand}; +use clap_verbosity_flag::Verbosity; +use golem_examples::model::{ExampleName, GuestLanguage, GuestLanguageTier, PackageName}; + +pub mod api_definition; +pub mod api_deployment; +pub mod component; +pub mod worker; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum OssCommand { + /// Upload and manage Golem components + #[command()] + Component { + #[command(subcommand)] + subcommand: ComponentSubCommand, + }, + + /// Manage Golem workers + #[command()] + Worker { + #[command(subcommand)] + subcommand: WorkerSubcommand, + }, + + /// Create a new Golem component from built-in examples + #[command()] + New { + /// Name of the example to use + #[arg(short, long)] + example: ExampleName, + + /// The new component's name + #[arg(short, long)] + component_name: golem_examples::model::ComponentName, + + /// The package name of the generated component (in namespace:name format) + #[arg(short, long)] + package_name: Option, + }, + + /// Lists the built-in examples available for creating new components + #[command()] + ListExamples { + /// The minimum language tier to include in the list + #[arg(short, long)] + min_tier: Option, + + /// Filter examples by a given guest language + #[arg(short, long)] + language: Option, + }, + + /// WASM RPC stub generator + #[cfg(feature = "stubgen")] + Stubgen { + #[command(subcommand)] + subcommand: golem_wasm_rpc_stubgen::Command, + }, + + /// Manage Golem api definitions + #[command()] + ApiDefinition { + #[command(subcommand)] + subcommand: ApiDefinitionSubcommand, + }, + + /// Manage Golem api deployments + #[command()] + ApiDeployment { + #[command(subcommand)] + subcommand: ApiDeploymentSubcommand, + }, +} + +#[derive(Parser, Debug)] +#[command(author, version = option_env ! ("VERSION").unwrap_or(env ! ("CARGO_PKG_VERSION")), about, long_about, rename_all = "kebab-case")] +/// Command line interface for OSS version of Golem. +/// +/// For Golem Cloud client see golem-cloud-cli instead: https://github.com/golemcloud/golem-cloud-cli +pub struct GolemOssCommand { + #[command(flatten)] + pub verbosity: Verbosity, + + #[arg(short = 'F', long, default_value = "text")] + pub format: Format, + + #[arg(short = 'u', long)] + /// Golem base url. Default: GOLEM_BASE_URL environment variable or http://localhost:9881. + /// + /// You can also specify different URLs for different services + /// via GOLEM_COMPONENT_BASE_URL and GOLEM_WORKER_BASE_URL + /// environment variables. + pub golem_url: Option, + + #[command(subcommand)] + pub command: OssCommand, +} diff --git a/golem-cli/src/oss/command/api_definition.rs b/golem-cli/src/oss/command/api_definition.rs new file mode 100644 index 000000000..04e4d89b0 --- /dev/null +++ b/golem-cli/src/oss/command/api_definition.rs @@ -0,0 +1,106 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::{ + ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult, PathBufOrStdin, +}; +use crate::oss::model::OssContext; +use crate::service::api_definition::ApiDefinitionService; +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ApiDefinitionSubcommand { + /// Lists all api definitions + #[command()] + List { + /// Api definition id to get all versions. Optional. + #[arg(short, long)] + id: Option, + }, + + /// Creates an api definition + /// + /// Golem API definition file format expected + #[command()] + Add { + /// The Golem API definition file + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Updates an api definition + /// + /// Golem API definition file format expected + #[command()] + Update { + /// The Golem API definition file + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Import OpenAPI file as api definition + #[command()] + Import { + /// The OpenAPI json or yaml file to be used as the api definition + /// + /// Json format expected unless file name ends up in `.yaml` + #[arg(value_hint = clap::ValueHint::FilePath)] + definition: PathBufOrStdin, // TODO: validate exists + }, + + /// Retrieves metadata about an existing api definition + #[command()] + Get { + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + + /// Version of the api definition + #[arg(short = 'V', long)] + version: ApiDefinitionVersion, + }, + + /// Deletes an existing api definition + #[command()] + Delete { + /// Api definition id + #[arg(short, long)] + id: ApiDefinitionId, + + /// Version of the api definition + #[arg(short = 'V', long)] + version: ApiDefinitionVersion, + }, +} + +impl ApiDefinitionSubcommand { + pub async fn handle( + self, + service: &(dyn ApiDefinitionService + Send + Sync), + ) -> Result { + let ctx = &OssContext::EMPTY; + + match self { + ApiDefinitionSubcommand::Get { id, version } => service.get(id, version, ctx).await, + ApiDefinitionSubcommand::Add { definition } => service.add(definition, ctx).await, + ApiDefinitionSubcommand::Update { definition } => service.update(definition, ctx).await, + ApiDefinitionSubcommand::Import { definition } => service.import(definition, ctx).await, + ApiDefinitionSubcommand::List { id } => service.list(id, ctx).await, + ApiDefinitionSubcommand::Delete { id, version } => { + service.delete(id, version, ctx).await + } + } + } +} diff --git a/golem-cli/src/api_deployment.rs b/golem-cli/src/oss/command/api_deployment.rs similarity index 57% rename from golem-cli/src/api_deployment.rs rename to golem-cli/src/oss/command/api_deployment.rs index 964fb55cf..7f253ba73 100644 --- a/golem-cli/src/api_deployment.rs +++ b/golem-cli/src/oss/command/api_deployment.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::clients::api_deployment::ApiDeploymentClient; use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult}; -use async_trait::async_trait; +use crate::oss::model::OssContext; +use crate::service::api_deployment::ApiDeploymentService; use clap::Subcommand; #[derive(Subcommand, Debug)] @@ -63,44 +63,23 @@ pub enum ApiDeploymentSubcommand { }, } -#[async_trait] -pub trait ApiDeploymentHandler { - async fn handle(&self, subcommand: ApiDeploymentSubcommand) -> Result; -} - -pub struct ApiDeploymentHandlerLive { - pub client: C, -} +impl ApiDeploymentSubcommand { + pub async fn handle( + self, + service: &(dyn ApiDeploymentService + Send + Sync), + ) -> Result { + let ctx = &OssContext::EMPTY; -#[async_trait] -impl ApiDeploymentHandler for ApiDeploymentHandlerLive { - async fn handle(&self, subcommand: ApiDeploymentSubcommand) -> Result { - match subcommand { + match self { ApiDeploymentSubcommand::Deploy { id, version, host, subdomain, - } => { - let deployment = self.client.deploy(&id, &version, &host, subdomain).await?; - - Ok(GolemResult::Ok(Box::new(deployment))) - } - ApiDeploymentSubcommand::Get { site } => { - let deployment = self.client.get(&site).await?; - - Ok(GolemResult::Ok(Box::new(deployment))) - } - ApiDeploymentSubcommand::List { id } => { - let deployments = self.client.list(&id).await?; - - Ok(GolemResult::Ok(Box::new(deployments))) - } - ApiDeploymentSubcommand::Delete { site } => { - let res = self.client.delete(&site).await?; - - Ok(GolemResult::Str(res)) - } + } => service.deploy(id, version, host, subdomain, ctx).await, + ApiDeploymentSubcommand::Get { site } => service.get(site).await, + ApiDeploymentSubcommand::List { id } => service.list(id, ctx).await, + ApiDeploymentSubcommand::Delete { site } => service.delete(site).await, } } } diff --git a/golem-cli/src/oss/command/component.rs b/golem-cli/src/oss/command/component.rs new file mode 100644 index 000000000..1e5f60720 --- /dev/null +++ b/golem-cli/src/oss/command/component.rs @@ -0,0 +1,94 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::{ComponentIdOrName, ComponentName, GolemError, GolemResult, PathBufOrStdin}; +use crate::oss::model::OssContext; +use crate::service::component::ComponentService; +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum ComponentSubCommand { + /// Creates a new component with a given name by uploading the component WASM + #[command()] + Add { + /// Name of the newly created component + #[arg(short, long)] + component_name: ComponentName, + + /// The WASM file to be used as a Golem component + #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] + component_file: PathBufOrStdin, // TODO: validate exists + }, + + /// Updates an existing component by uploading a new version of its WASM + #[command()] + Update { + /// The component name or identifier to update + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// The WASM file to be used as a new version of the Golem component + #[arg(value_name = "component-file", value_hint = clap::ValueHint::FilePath)] + component_file: PathBufOrStdin, // TODO: validate exists + }, + + /// Lists the existing components + #[command()] + List { + /// Optionally look for only components matching a given name + #[arg(short, long)] + component_name: Option, + }, + /// Get component + #[command()] + Get { + /// The Golem component id or name + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// The version of the component + #[arg(short = 't', long)] + version: Option, + }, +} + +impl ComponentSubCommand { + pub async fn handle( + self, + service: &(dyn ComponentService + Send + Sync), + ) -> Result { + match self { + ComponentSubCommand::Add { + component_name, + component_file, + } => service.add(component_name, component_file, None).await, + ComponentSubCommand::Update { + component_id_or_name, + component_file, + } => { + service + .update(component_id_or_name, component_file, None) + .await + } + ComponentSubCommand::List { component_name } => { + service.list(component_name, None).await + } + ComponentSubCommand::Get { + component_id_or_name, + version, + } => service.get(component_id_or_name, version, None).await, + } + } +} diff --git a/golem-cli/src/oss/command/worker.rs b/golem-cli/src/oss/command/worker.rs new file mode 100644 index 000000000..a32914b85 --- /dev/null +++ b/golem-cli/src/oss/command/worker.rs @@ -0,0 +1,378 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::builder::ValueParser; +use clap::Subcommand; +use golem_client::model::ScanCursor; + +use crate::model::{ + ComponentIdOrName, Format, GolemError, GolemResult, IdempotencyKey, JsonValueParser, + WorkerName, WorkerUpdateMode, +}; +use crate::oss::model::OssContext; +use crate::parse_key_val; +use crate::service::worker::WorkerService; + +#[derive(Subcommand, Debug)] +#[command()] +pub enum WorkerSubcommand { + /// Creates a new idle worker + #[command()] + Add { + /// The Golem component to use for the worker, identified by either its name or its component ID + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the newly created worker + #[arg(short, long)] + worker_name: WorkerName, + + /// List of environment variables (key-value pairs) passed to the worker + #[arg(short, long, value_parser = parse_key_val, value_name = "ENV=VAL")] + env: Vec<(String, String)>, + + /// List of command line arguments passed to the worker + #[arg(value_name = "args")] + args: Vec, + }, + + /// Generates an idempotency key for achieving at-most-one invocation when doing retries + #[command()] + IdempotencyKey {}, + + /// Invokes a worker and waits for its completion + #[command()] + InvokeAndAwait { + /// The Golem component the worker to be invoked belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + + /// A pre-generated idempotency key + #[arg(short = 'k', long)] + idempotency_key: Option, + + /// Name of the function to be invoked + #[arg(short, long)] + function: String, + + /// JSON array representing the parameters to be passed to the function + #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] + parameters: Option, + + /// Function parameter in WAVE format + /// + /// You can specify this argument multiple times for multiple parameters. + #[arg( + short = 'p', + long = "param", + value_name = "wave", + conflicts_with = "parameters" + )] + wave: Vec, + + /// Enables the STDIO cal;ing convention, passing the parameters through stdin instead of a typed exported interface + #[arg(short = 's', long, default_value_t = false)] + use_stdio: bool, + }, + + /// Triggers a function invocation on a worker without waiting for its completion + #[command()] + Invoke { + /// The Golem component the worker to be invoked belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + + /// A pre-generated idempotency key + #[arg(short = 'k', long)] + idempotency_key: Option, + + /// Name of the function to be invoked + #[arg(short, long)] + function: String, + + /// JSON array representing the parameters to be passed to the function + #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] + parameters: Option, + + /// Function parameter in WAVE format + /// + /// You can specify this argument multiple times for multiple parameters. + #[arg( + short = 'p', + long = "param", + value_name = "wave", + conflicts_with = "parameters" + )] + wave: Vec, + }, + + /// Connect to a worker and live stream its standard output, error and log channels + #[command()] + Connect { + /// The Golem component the worker to be connected to belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Interrupts a running worker + #[command()] + Interrupt { + /// The Golem component the worker to be interrupted belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Simulates a crash on a worker for testing purposes. + /// + /// The worker starts recovering and resuming immediately. + #[command()] + SimulatedCrash { + /// The Golem component the worker to be crashed belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Deletes a worker + #[command()] + Delete { + /// The Golem component the worker to be deleted belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + + /// Retrieves metadata about an existing worker + #[command()] + Get { + /// The Golem component the worker to be retrieved belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker + #[arg(short, long)] + worker_name: WorkerName, + }, + /// Retrieves metadata about an existing workers in a component + #[command()] + List { + /// The Golem component the workers to be retrieved belongs to + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Filter for worker metadata in form of `property op value`. + /// + /// Filter examples: `name = worker-name`, `version >= 0`, `status = Running`, `env.var1 = value`. + /// Can be used multiple times (AND condition is applied between them) + #[arg(short, long)] + filter: Option>, + + /// Position where to start listing, if not provided, starts from the beginning + /// + /// It is used to get the next page of results. To get next page, use the cursor returned in the response. + /// The cursor has the format 'layer/position' where both layer and position are numbers. + #[arg(short = 'P', long, value_parser = parse_cursor)] + cursor: Option, + + /// Count of listed values, if count is not provided, returns all values + #[arg(short = 'n', long)] + count: Option, + + /// Precision in relation to worker status, if true, calculate the most up-to-date status for each worker, default is false + #[arg(short, long)] + precise: Option, + }, + /// Updates a worker + #[command()] + Update { + /// The Golem component of the worker, identified by either its name or its component ID + #[command(flatten)] + component_id_or_name: ComponentIdOrName, + + /// Name of the worker to update + #[arg(short, long)] + worker_name: WorkerName, + + /// Update mode - auto or manual + #[arg(short, long)] + mode: WorkerUpdateMode, + + /// The new version of the updated worker + #[arg(short = 't', long)] + target_version: u64, + }, +} + +impl WorkerSubcommand { + pub async fn handle( + self, + format: Format, + service: &(dyn WorkerService + Send + Sync), + ) -> Result { + match self { + WorkerSubcommand::Add { + component_id_or_name, + worker_name, + env, + args, + } => { + service + .add(component_id_or_name, worker_name, env, args, None) + .await + } + WorkerSubcommand::IdempotencyKey {} => service.idempotency_key().await, + WorkerSubcommand::InvokeAndAwait { + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + use_stdio, + } => { + service + .invoke_and_await( + format, + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + use_stdio, + None, + ) + .await + } + WorkerSubcommand::Invoke { + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + } => { + service + .invoke( + component_id_or_name, + worker_name, + idempotency_key, + function, + parameters, + wave, + None, + ) + .await + } + WorkerSubcommand::Connect { + component_id_or_name, + worker_name, + } => { + service + .connect(component_id_or_name, worker_name, None) + .await + } + WorkerSubcommand::Interrupt { + component_id_or_name, + worker_name, + } => { + service + .interrupt(component_id_or_name, worker_name, None) + .await + } + WorkerSubcommand::SimulatedCrash { + component_id_or_name, + worker_name, + } => { + service + .simulated_crash(component_id_or_name, worker_name, None) + .await + } + WorkerSubcommand::Delete { + component_id_or_name, + worker_name, + } => { + service + .delete(component_id_or_name, worker_name, None) + .await + } + WorkerSubcommand::Get { + component_id_or_name, + worker_name, + } => service.get(component_id_or_name, worker_name, None).await, + WorkerSubcommand::List { + component_id_or_name, + filter, + count, + cursor, + precise, + } => { + service + .list(component_id_or_name, filter, count, cursor, precise, None) + .await + } + WorkerSubcommand::Update { + component_id_or_name, + worker_name, + target_version, + mode, + } => { + service + .update( + component_id_or_name, + worker_name, + target_version, + mode, + None, + ) + .await + } + } + } +} + +fn parse_cursor(s: &str) -> Result> { + let parts = s.split('/').collect::>(); + + if parts.len() != 2 { + return Err(format!("Invalid cursor format: {}", s).into()); + } + + Ok(ScanCursor { + layer: parts[0].parse()?, + cursor: parts[1].parse()?, + }) +} \ No newline at end of file diff --git a/golem-cli/src/oss/factory.rs b/golem-cli/src/oss/factory.rs new file mode 100644 index 000000000..5c0e6d41c --- /dev/null +++ b/golem-cli/src/oss/factory.rs @@ -0,0 +1,148 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::api_definition::ApiDefinitionClient; +use crate::clients::api_deployment::ApiDeploymentClient; +use crate::clients::component::ComponentClient; +use crate::clients::health_check::HealthCheckClient; +use crate::clients::worker::WorkerClient; +use crate::factory::{FactoryWithAuth, ServiceFactory}; +use crate::model::GolemError; +use crate::oss::clients::api_definition::ApiDefinitionClientLive; +use crate::oss::clients::api_deployment::ApiDeploymentClientLive; +use crate::oss::clients::component::ComponentClientLive; +use crate::oss::clients::health_check::HealthCheckClientLive; +use crate::oss::clients::worker::WorkerClientLive; +use crate::oss::model::OssContext; +use golem_client::Context; +use url::Url; + +#[derive(Debug, Clone)] +pub struct OssServiceFactory { + pub component_url: Url, + pub worker_url: Url, + pub allow_insecure: bool, +} + +impl OssServiceFactory { + fn client(&self) -> Result { + let mut builder = reqwest::Client::builder(); + if self.allow_insecure { + builder = builder.danger_accept_invalid_certs(true); + } + + Ok(builder.connection_verbose(true).build()?) + } + + fn component_context(&self) -> Result { + Ok(Context { + base_url: self.component_url.clone(), + client: self.client()?, + }) + } + fn worker_context(&self) -> Result { + Ok(Context { + base_url: self.worker_url.clone(), + client: self.client()?, + }) + } +} + +impl ServiceFactory for OssServiceFactory { + type SecurityContext = OssContext; + type ProjectContext = OssContext; + + fn with_auth( + &self, + auth: &Self::SecurityContext, + ) -> FactoryWithAuth { + FactoryWithAuth { + auth: *auth, + factory: Box::new(self.clone()), + } + } + + fn component_client( + &self, + _auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ComponentClientLive { + client: golem_client::api::ComponentClientLive { + context: self.component_context()?, + }, + })) + } + + fn worker_client( + &self, + _auth: &Self::SecurityContext, + ) -> Result, GolemError> { + Ok(Box::new(WorkerClientLive { + client: golem_client::api::WorkerClientLive { + context: self.worker_context()?, + }, + context: self.worker_context()?, + allow_insecure: self.allow_insecure, + })) + } + + fn api_definition_client( + &self, + _auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDefinitionClientLive { + client: golem_client::api::ApiDefinitionClientLive { + context: self.worker_context()?, + }, + })) + } + + fn api_deployment_client( + &self, + _auth: &Self::SecurityContext, + ) -> Result< + Box + Send + Sync>, + GolemError, + > { + Ok(Box::new(ApiDeploymentClientLive { + client: golem_client::api::ApiDeploymentClientLive { + context: self.worker_context()?, + }, + })) + } + + fn health_check_clients( + &self, + _auth: &Self::SecurityContext, + ) -> Result>, GolemError> { + Ok(vec![ + Box::new(HealthCheckClientLive { + client: golem_client::api::HealthCheckClientLive { + context: self.component_context()?, + }, + }), + Box::new(HealthCheckClientLive { + client: golem_client::api::HealthCheckClientLive { + context: self.worker_context()?, + }, + }), + ]) + } +} diff --git a/golem-cli/src/oss/model.rs b/golem-cli/src/oss/model.rs new file mode 100644 index 000000000..5f80d7327 --- /dev/null +++ b/golem-cli/src/oss/model.rs @@ -0,0 +1,28 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone, Copy)] +pub struct OssContext {} + +impl OssContext { + pub const EMPTY: OssContext = OssContext {}; +} + +impl Display for OssContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "OSS context") + } +} diff --git a/golem-cli/src/service.rs b/golem-cli/src/service.rs new file mode 100644 index 000000000..2f01d4c0b --- /dev/null +++ b/golem-cli/src/service.rs @@ -0,0 +1,19 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod api_definition; +pub mod api_deployment; +pub mod component; +pub mod version; +pub mod worker; diff --git a/golem-cli/src/service/api_definition.rs b/golem-cli/src/service/api_definition.rs new file mode 100644 index 000000000..001fd19f1 --- /dev/null +++ b/golem-cli/src/service/api_definition.rs @@ -0,0 +1,130 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::api_definition::ApiDefinitionClient; +use crate::model::text::{ + ApiDefinitionAddRes, ApiDefinitionGetRes, ApiDefinitionImportRes, ApiDefinitionUpdateRes, +}; +use crate::model::{ + ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult, PathBufOrStdin, +}; +use async_trait::async_trait; + +#[async_trait] +pub trait ApiDefinitionService { + type ProjectContext; + async fn get( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result; + async fn add( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; + async fn update( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; + async fn import( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result; + async fn list( + &self, + id: Option, + project: &Self::ProjectContext, + ) -> Result; + async fn delete( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result; +} + +pub struct ApiDefinitionServiceLive { + pub client: Box + Send + Sync>, +} + +#[async_trait] +impl ApiDefinitionService + for ApiDefinitionServiceLive +{ + type ProjectContext = ProjectContext; + + async fn get( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result { + let definition = self.client.get(id, version, project).await?; + Ok(GolemResult::Ok(Box::new(ApiDefinitionGetRes(definition)))) + } + + async fn add( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + let definition = self.client.create(definition, project).await?; + Ok(GolemResult::Ok(Box::new(ApiDefinitionAddRes(definition)))) + } + + async fn update( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + let definition = self.client.update(definition, project).await?; + Ok(GolemResult::Ok(Box::new(ApiDefinitionUpdateRes( + definition, + )))) + } + + async fn import( + &self, + definition: PathBufOrStdin, + project: &Self::ProjectContext, + ) -> Result { + let definition = self.client.import(definition, project).await?; + Ok(GolemResult::Ok(Box::new(ApiDefinitionImportRes( + definition, + )))) + } + + async fn list( + &self, + id: Option, + project: &Self::ProjectContext, + ) -> Result { + let definitions = self.client.list(id.as_ref(), project).await?; + Ok(GolemResult::Ok(Box::new(definitions))) + } + + async fn delete( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + project: &Self::ProjectContext, + ) -> Result { + let result = self.client.delete(id, version, project).await?; + Ok(GolemResult::Str(result)) + } +} diff --git a/golem-cli/src/service/api_deployment.rs b/golem-cli/src/service/api_deployment.rs new file mode 100644 index 000000000..745f2adcd --- /dev/null +++ b/golem-cli/src/service/api_deployment.rs @@ -0,0 +1,87 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::api_deployment::ApiDeploymentClient; +use crate::model::{ApiDefinitionId, ApiDefinitionVersion, GolemError, GolemResult}; +use async_trait::async_trait; + +#[async_trait] +pub trait ApiDeploymentService { + type ProjectContext; + + async fn deploy( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + host: String, + subdomain: Option, + project: &Self::ProjectContext, + ) -> Result; + async fn get(&self, site: String) -> Result; + async fn list( + &self, + id: ApiDefinitionId, + project: &Self::ProjectContext, + ) -> Result; + async fn delete(&self, site: String) -> Result; +} + +pub struct ApiDeploymentServiceLive { + pub client: Box + Send + Sync>, +} + +#[async_trait] +impl ApiDeploymentService + for ApiDeploymentServiceLive +{ + type ProjectContext = ProjectContext; + + async fn deploy( + &self, + id: ApiDefinitionId, + version: ApiDefinitionVersion, + host: String, + subdomain: Option, + project: &Self::ProjectContext, + ) -> Result { + let deployment = self + .client + .deploy(&id, &version, &host, subdomain, project) + .await?; + + Ok(GolemResult::Ok(Box::new(deployment))) + } + + async fn get(&self, site: String) -> Result { + let deployment = self.client.get(&site).await?; + + Ok(GolemResult::Ok(Box::new(deployment))) + } + + async fn list( + &self, + id: ApiDefinitionId, + project: &Self::ProjectContext, + ) -> Result { + let deployments = self.client.list(&id, project).await?; + + Ok(GolemResult::Ok(Box::new(deployments))) + } + + async fn delete(&self, site: String) -> Result { + let res = self.client.delete(&site).await?; + + Ok(GolemResult::Str(res)) + } +} diff --git a/golem-cli/src/service/component.rs b/golem-cli/src/service/component.rs new file mode 100644 index 000000000..c16352e9a --- /dev/null +++ b/golem-cli/src/service/component.rs @@ -0,0 +1,203 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::component::ComponentClient; +use crate::model::component::{Component, ComponentView}; +use crate::model::text::{ComponentAddView, ComponentGetView, ComponentUpdateView}; +use crate::model::{ + ComponentId, ComponentIdOrName, ComponentName, GolemError, GolemResult, PathBufOrStdin, +}; +use async_trait::async_trait; +use indoc::formatdoc; +use itertools::Itertools; +use std::fmt::Display; + +#[async_trait] +pub trait ComponentService { + type ProjectContext: Send + Sync; + + async fn add( + &self, + component_name: ComponentName, + component_file: PathBufOrStdin, + project: Option, + ) -> Result; + async fn update( + &self, + component_id_or_name: ComponentIdOrName, + component_file: PathBufOrStdin, + project: Option, + ) -> Result; + async fn list( + &self, + component_name: Option, + project: Option, + ) -> Result; + async fn get( + &self, + component_id_or_name: ComponentIdOrName, + version: Option, + project: Option, + ) -> Result; + async fn resolve_id( + &self, + reference: ComponentIdOrName, + project: Option, + ) -> Result; + async fn get_metadata( + &self, + component_id: &ComponentId, + version: u64, + ) -> Result; + async fn get_latest_metadata( + &self, + component_id: &ComponentId, + ) -> Result; +} + +pub struct ComponentServiceLive { + pub client: Box + Send + Sync>, +} + +#[async_trait] +impl ComponentService + for ComponentServiceLive +{ + type ProjectContext = ProjectContext; + + async fn add( + &self, + component_name: ComponentName, + component_file: PathBufOrStdin, + project: Option, + ) -> Result { + let component = self + .client + .add(component_name, component_file, &project) + .await?; + let view: ComponentView = component.into(); + + Ok(GolemResult::Ok(Box::new(ComponentAddView(view)))) + } + + async fn update( + &self, + component_id_or_name: ComponentIdOrName, + component_file: PathBufOrStdin, + project: Option, + ) -> Result { + let id = self.resolve_id(component_id_or_name, project).await?; + let component = self.client.update(id, component_file).await?; + let view: ComponentView = component.into(); + + Ok(GolemResult::Ok(Box::new(ComponentUpdateView(view)))) + } + + async fn list( + &self, + component_name: Option, + project: Option, + ) -> Result { + let components = self.client.find(component_name, &project).await?; + let views: Vec = components.into_iter().map(|t| t.into()).collect(); + + Ok(GolemResult::Ok(Box::new(views))) + } + + async fn get( + &self, + component_id_or_name: ComponentIdOrName, + version: Option, + project: Option, + ) -> Result { + let component_id = self.resolve_id(component_id_or_name, project).await?; + let component = match version { + Some(v) => self.get_metadata(&component_id, v).await?, + None => self.get_latest_metadata(&component_id).await?, + }; + let view: ComponentView = component.into(); + Ok(GolemResult::Ok(Box::new(ComponentGetView(view)))) + } + + async fn resolve_id( + &self, + reference: ComponentIdOrName, + project_context: Option, + ) -> Result { + match reference { + ComponentIdOrName::Id(id) => Ok(id), + ComponentIdOrName::Name(name) => { + let components = self + .client + .find(Some(name.clone()), &project_context) + .await?; + let components: Vec = components + .into_iter() + .group_by(|c| c.versioned_component_id.component_id) + .into_iter() + .map(|(_, group)| { + group + .max_by_key(|c| c.versioned_component_id.version) + .unwrap() + }) + .collect(); + + if components.len() > 1 { + let project_msg = match project_context { + None => "".to_string(), + Some(project) => format!(" in project {project}"), + }; + let component_name = name.0; + let ids: Vec = components + .into_iter() + .map(|c| c.versioned_component_id.component_id.to_string()) + .collect(); + Err(GolemError(formatdoc!( + " + Multiple components found for name {component_name}{project_msg}: + {} + Use explicit --component-id + ", + ids.join(", ") + ))) + } else { + match components.first() { + None => { + let component_name = name.0; + Err(GolemError(format!("Can't find component {component_name}"))) + } + Some(component) => { + Ok(ComponentId(component.versioned_component_id.component_id)) + } + } + } + } + } + } + + async fn get_metadata( + &self, + component_id: &ComponentId, + version: u64, + ) -> Result { + self.client.get_metadata(component_id, version).await + } + + async fn get_latest_metadata( + &self, + component_id: &ComponentId, + ) -> Result { + self.client.get_latest_metadata(component_id).await + } +} diff --git a/golem-cli/src/service/version.rs b/golem-cli/src/service/version.rs new file mode 100644 index 000000000..4b4c02de5 --- /dev/null +++ b/golem-cli/src/service/version.rs @@ -0,0 +1,62 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::health_check::HealthCheckClient; +use crate::model::{GolemError, GolemResult}; +use async_trait::async_trait; +use std::cmp::Ordering; +use version_compare::Version; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[async_trait] +pub trait VersionService { + async fn check(&self) -> Result; +} + +pub struct VersionServiceLive { + pub clients: Vec>, +} + +#[async_trait] +impl VersionService for VersionServiceLive { + async fn check(&self) -> Result { + let mut versions = Vec::with_capacity(self.clients.len()); + for client in &self.clients { + versions.push(client.version().await?) + } + + let srv_versions = versions + .iter() + .map(|v| Version::from(v.version.as_str()).unwrap()) + .collect::>(); + + let cli_version = Version::from(VERSION).unwrap(); + + let warning = |cli_version: Version, server_version: &Version| -> String { + format!("Warning: golem-cli {} is older than the targeted Golem servers ({})\nInstall the matching version with:\ncargo install golem-cli@{}\n", cli_version.as_str(), server_version.as_str(), server_version.as_str()).to_string() + }; + + let newer = srv_versions + .iter() + .filter(|&v| v > &cli_version) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + if let Some(version) = newer { + Err(GolemError(warning(cli_version, version))) + } else { + Ok(GolemResult::Str("No updates found".to_string())) + } + } +} diff --git a/golem-cli/src/service/worker.rs b/golem-cli/src/service/worker.rs new file mode 100644 index 000000000..01fd2a838 --- /dev/null +++ b/golem-cli/src/service/worker.rs @@ -0,0 +1,596 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::clients::worker::WorkerClient; +use crate::model::component::{function_params_types, Component}; +use crate::model::invoke_result_view::InvokeResultView; +use crate::model::text::WorkerAddView; +use crate::model::wave::type_to_analysed; +use crate::model::{ + ComponentId, ComponentIdOrName, Format, GolemError, GolemResult, IdempotencyKey, + WorkerMetadata, WorkerName, WorkerUpdateMode, WorkersMetadataResponse, +}; +use crate::service::component::ComponentService; +use async_trait::async_trait; +use golem_client::model::{InvokeParameters, InvokeResult, ScanCursor, StringFilterComparator, Type, WorkerFilter, WorkerNameFilter}; +use golem_wasm_rpc::TypeAnnotatedValue; +use serde_json::Value; +use tokio::task::JoinHandle; +use tracing::{error, info}; +use uuid::Uuid; + +#[async_trait] +pub trait WorkerService { + type ProjectContext: Send + Sync; + + async fn add( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + env: Vec<(String, String)>, + args: Vec, + project: Option, + ) -> Result; + async fn idempotency_key(&self) -> Result { + let key = IdempotencyKey(Uuid::new_v4().to_string()); + Ok(GolemResult::Ok(Box::new(key))) + } + async fn invoke_and_await( + &self, + format: Format, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + idempotency_key: Option, + function: String, + parameters: Option, + wave: Vec, + use_stdio: bool, + project: Option, + ) -> Result; + async fn invoke( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + idempotency_key: Option, + function: String, + parameters: Option, + wave: Vec, + project: Option, + ) -> Result; + async fn connect( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result; + async fn interrupt( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result; + async fn simulated_crash( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result; + async fn delete( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result; + async fn get( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result; + async fn list( + &self, + component_id_or_name: ComponentIdOrName, + filter: Option>, + count: Option, + cursor: Option, + precise: Option, + project: Option, + ) -> Result; + async fn update( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + target_version: u64, + mode: WorkerUpdateMode, + project: Option, + ) -> Result; +} + +pub trait WorkerClientBuilder { + fn build(&self) -> Result, GolemError>; +} + +pub trait ComponentServiceBuilder { + fn build( + &self, + ) -> Result + Send + Sync>, GolemError>; +} + +pub struct WorkerServiceLive { + pub client: Box, + pub components: Box + Send + Sync>, + pub client_builder: Box, + pub component_service_builder: Box + Send + Sync>, +} + +// same as resolve_worker_component_version, but with no borrowing, so we can spawn it. +async fn resolve_worker_component_version_no_ref( + worker_client: Box, + component_service: Box + Send + Sync>, + component_id: ComponentId, + worker_name: WorkerName, +) -> Result, GolemError> { + resolve_worker_component_version( + worker_client.as_ref(), + component_service.as_ref(), + &component_id, + worker_name, + ) + .await +} + +async fn resolve_worker_component_version( + client: &(dyn WorkerClient + Send + Sync), + components: &(dyn ComponentService + Send + Sync), + component_id: &ComponentId, + worker_name: WorkerName, +) -> Result, GolemError> { + let worker_meta = client + .find_metadata( + component_id.clone(), + Some(WorkerFilter::Name(WorkerNameFilter { + comparator: StringFilterComparator::Equal, + value: worker_name.0, + })), + None, + Some(2), + Some(true), + ) + .await?; + + if worker_meta.workers.len() > 1 { + Err(GolemError( + "Multiple workers with the same name".to_string(), + )) + } else if let Some(worker) = worker_meta.workers.first() { + Ok(Some( + components + .get_metadata(component_id, worker.component_version) + .await?, + )) + } else { + Ok(None) + } +} + +fn wave_parameters_to_json( + wave: &[String], + component: &Component, + function: &str, +) -> Result { + let types = function_params_types(component, function)?; + + if wave.len() != types.len() { + return Err(GolemError(format!( + "Invalid number of wave parameters for function {function}. Expected {}, but got {}.", + types.len(), + wave.len() + ))); + } + + let params = wave + .iter() + .zip(types) + .map(|(param, typ)| parse_parameter(param, typ)) + .collect::, _>>()?; + + let json_params = params + .iter() + .map(golem_wasm_rpc::json::get_json_from_typed_value) + .collect::>(); + + Ok(Value::Array(json_params)) +} + +fn parse_parameter(wave: &str, typ: &Type) -> Result { + match wasm_wave::from_str(&type_to_analysed(typ), wave) { + Ok(value) => Ok(value), + Err(err) => Err(GolemError(format!( + "Failed to parse wave parameter {wave}: {err:?}" + ))), + } +} + +async fn resolve_parameters( + client: &(dyn WorkerClient + Send + Sync), + components: &(dyn ComponentService + Send + Sync), + component_id: &ComponentId, + worker_name: &WorkerName, + parameters: Option, + wave: Vec, + function: &str, +) -> Result<(Value, Option), GolemError> { + if let Some(parameters) = parameters { + Ok((parameters, None)) + } else if let Some(component) = + resolve_worker_component_version(client, components, component_id, worker_name.clone()) + .await? + { + let json = wave_parameters_to_json(&wave, &component, function)?; + + Ok((json, Some(component))) + } else { + info!("No worker found with name {worker_name}. Assuming it should be create with the latest component version"); + let component = components.get_latest_metadata(component_id).await?; + + let json = wave_parameters_to_json(&wave, &component, function)?; + + // We are not going to use this component for result parsing. + Ok((json, None)) + } +} + +async fn to_invoke_result_view( + res: InvokeResult, + async_component_request: AsyncComponentRequest, + client: &(dyn WorkerClient + Send + Sync), + components: &(dyn ComponentService + Send + Sync), + component_id: &ComponentId, + worker_name: &WorkerName, + function: &str, +) -> Result { + let component = match async_component_request { + AsyncComponentRequest::Empty => None, + AsyncComponentRequest::Resolved(component) => Some(component), + AsyncComponentRequest::Async(join_meta) => match join_meta.await.unwrap() { + Ok(Some(component)) => Some(component), + _ => None, + }, + }; + + let component = match component { + None => { + match resolve_worker_component_version( + client, + components, + component_id, + worker_name.clone(), + ) + .await + { + Ok(Some(component)) => component, + _ => { + error!("Failed to get worker metadata after successful call."); + + return Ok(InvokeResultView::Json(res.result)); + } + } + } + Some(component) => component, + }; + + Ok(InvokeResultView::try_parse_or_json( + res, &component, function, + )) +} + +enum AsyncComponentRequest { + Empty, + Resolved(Component), + Async(JoinHandle, GolemError>>), +} + +#[async_trait] +impl WorkerService for WorkerServiceLive { + type ProjectContext = ProjectContext; + + async fn add( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + env: Vec<(String, String)>, + args: Vec, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + let inst = self + .client + .new_worker(worker_name, component_id, args, env) + .await?; + + Ok(GolemResult::Ok(Box::new(WorkerAddView(inst)))) + } + + async fn invoke_and_await( + &self, + format: Format, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + idempotency_key: Option, + function: String, + parameters: Option, + wave: Vec, + use_stdio: bool, + project: Option, + ) -> Result { + let human_readable = format == Format::Text; + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + let (parameters, component_meta) = resolve_parameters( + self.client.as_ref(), + self.components.as_ref(), + &component_id, + &worker_name, + parameters, + wave, + &function, + ) + .await?; + + let async_component_request = if let Some(component) = component_meta { + AsyncComponentRequest::Resolved(component) + } else if human_readable { + let worker_client = self.client_builder.build()?; + let component_service = self.component_service_builder.build()?; + AsyncComponentRequest::Async(tokio::spawn(resolve_worker_component_version_no_ref( + worker_client, + component_service, + component_id.clone(), + worker_name.clone(), + ))) + } else { + AsyncComponentRequest::Empty + }; + + let res = self + .client + .invoke_and_await( + worker_name.clone(), + component_id.clone(), + function.clone(), + InvokeParameters { params: parameters }, + idempotency_key, + use_stdio, + ) + .await?; + + if human_readable { + let view = to_invoke_result_view( + res, + async_component_request, + self.client.as_ref(), + self.components.as_ref(), + &component_id, + &worker_name, + &function, + ) + .await?; + + Ok(GolemResult::Ok(Box::new(view))) + } else { + Ok(GolemResult::Json(res.result)) + } + } + + async fn invoke( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + idempotency_key: Option, + function: String, + parameters: Option, + wave: Vec, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + let (parameters, _) = resolve_parameters( + self.client.as_ref(), + self.components.as_ref(), + &component_id, + &worker_name, + parameters, + wave, + &function, + ) + .await?; + + self.client + .invoke( + worker_name, + component_id, + function, + InvokeParameters { params: parameters }, + idempotency_key, + ) + .await?; + + Ok(GolemResult::Str("Invoked".to_string())) + } + + async fn connect( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + self.client.connect(worker_name, component_id).await?; + + Err(GolemError("Unexpected connection closure".to_string())) + } + + async fn interrupt( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + self.client.interrupt(worker_name, component_id).await?; + + Ok(GolemResult::Str("Interrupted".to_string())) + } + + async fn simulated_crash( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + self.client + .simulated_crash(worker_name, component_id) + .await?; + + Ok(GolemResult::Str("Done".to_string())) + } + + async fn delete( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + self.client.delete(worker_name, component_id).await?; + + Ok(GolemResult::Str("Deleted".to_string())) + } + + async fn get( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + let response = self.client.get_metadata(worker_name, component_id).await?; + + Ok(GolemResult::Ok(Box::new(response))) + } + + async fn list( + &self, + component_id_or_name: ComponentIdOrName, + filter: Option>, + count: Option, + cursor: Option, + precise: Option, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + + if count.is_some() { + let response = self + .client + .list_metadata(component_id, filter, cursor, count, precise) + .await?; + + Ok(GolemResult::Ok(Box::new(response))) + } else { + let mut workers: Vec = vec![]; + let mut new_cursor = cursor; + + loop { + let response = self + .client + .list_metadata( + component_id.clone(), + filter.clone(), + new_cursor, + Some(50), + precise, + ) + .await?; + + workers.extend(response.workers); + + new_cursor = response.cursor; + + if new_cursor.is_none() { + break; + } + } + + Ok(GolemResult::Ok(Box::new(WorkersMetadataResponse { + workers, + cursor: None, + }))) + } + } + + async fn update( + &self, + component_id_or_name: ComponentIdOrName, + worker_name: WorkerName, + target_version: u64, + mode: WorkerUpdateMode, + project: Option, + ) -> Result { + let component_id = self + .components + .resolve_id(component_id_or_name, project) + .await?; + let _ = self + .client + .update(worker_name, component_id, mode, target_version) + .await?; + + Ok(GolemResult::Str("Updated".to_string())) + } +} diff --git a/golem-cli/src/stubgen.rs b/golem-cli/src/stubgen.rs new file mode 100644 index 000000000..d09c8a039 --- /dev/null +++ b/golem-cli/src/stubgen.rs @@ -0,0 +1,39 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::{GolemError, GolemResult}; +use golem_wasm_rpc_stubgen::Command; + +pub async fn handle_stubgen(command: Command) -> Result { + match command { + Command::Generate(args) => golem_wasm_rpc_stubgen::generate(args) + .map_err(|err| GolemError(format!("{err}"))) + .map(|_| GolemResult::Str("Done".to_string())), + Command::Build(args) => golem_wasm_rpc_stubgen::build(args) + .await + .map_err(|err| GolemError(format!("{err}"))) + .map(|_| GolemResult::Str("Done".to_string())), + Command::AddStubDependency(args) => golem_wasm_rpc_stubgen::add_stub_dependency(args) + .map_err(|err| GolemError(format!("{err}"))) + .map(|_| GolemResult::Str("Done".to_string())), + Command::Compose(args) => golem_wasm_rpc_stubgen::compose(args) + .map_err(|err| GolemError(format!("{err}"))) + .map(|_| GolemResult::Str("Done".to_string())), + Command::InitializeWorkspace(args) => { + golem_wasm_rpc_stubgen::initialize_workspace(args, "golem-cli", &["stubgen"]) + .map_err(|err| GolemError(format!("{err}"))) + .map(|_| GolemResult::Str("Done".to_string())) + } + } +} diff --git a/golem-cli/src/version.rs b/golem-cli/src/version.rs deleted file mode 100644 index 258a72d75..000000000 --- a/golem-cli/src/version.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2024 Golem Cloud -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use async_trait::async_trait; -use version_compare::Version; - -use crate::clients::health_check::HealthCheckClient; -use crate::model::{GolemError, GolemResult}; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[async_trait] -pub trait VersionHandler { - async fn check(&self) -> Result; -} - -pub struct VersionHandlerLive< - T: HealthCheckClient + Send + Sync, - W: HealthCheckClient + Send + Sync, -> { - pub component_client: T, - pub worker_client: W, -} - -#[async_trait] -impl VersionHandler - for VersionHandlerLive -{ - async fn check(&self) -> Result { - let component_version_info = self.component_client.version().await?; - let worker_version_info = self.worker_client.version().await?; - - let cli_version = Version::from(VERSION).unwrap(); - let component_version = Version::from(component_version_info.version.as_str()).unwrap(); - let worker_version = Version::from(worker_version_info.version.as_str()).unwrap(); - - let warning = |cli_version: Version, server_version: Version| -> String { - format!("Warning: golem-cli {} is older than the targeted Golem servers ({})\nInstall the matching version with:\ncargo install golem-cli@{}\n", cli_version.as_str(), server_version.as_str(), server_version.as_str()).to_string() - }; - - if cli_version < component_version && cli_version < worker_version { - if component_version > worker_version { - Err(GolemError(warning(cli_version, component_version))) - } else { - Err(GolemError(warning(cli_version, worker_version))) - } - } else if cli_version < component_version { - Err(GolemError(warning(cli_version, component_version))) - } else if cli_version < worker_version { - Err(GolemError(warning(cli_version, worker_version))) - } else { - Ok(GolemResult::Str("No updates found".to_string())) - } - } -} - -#[cfg(test)] -mod tests { - - use crate::model::GolemResult; - use crate::{ - clients::health_check::HealthCheckClient, - model::GolemError, - version::{VersionHandler, VersionHandlerLive}, - }; - use async_trait::async_trait; - use golem_client::model::VersionInfo; - - pub struct HealthCheckClientTest { - version: &'static str, - } - - #[async_trait] - impl HealthCheckClient for HealthCheckClientTest { - async fn version(&self) -> Result { - Ok(VersionInfo { - version: self.version.to_string(), - }) - } - } - - fn client(v: &'static str) -> HealthCheckClientTest { - HealthCheckClientTest { version: v } - } - - fn warning(server_version: &str) -> String { - format!("Warning: golem-cli 0.0.0 is older than the targeted Golem servers ({})\nInstall the matching version with:\ncargo install golem-cli@{}\n", server_version, server_version).to_string() - } - - async fn check_version( - component_version: &'static str, - worker_version: &'static str, - ) -> String { - let update_srv = VersionHandlerLive { - component_client: client(component_version), - worker_client: client(worker_version), - }; - - let checked = update_srv.check().await; - match checked { - Ok(GolemResult::Str(s)) => s, - Err(e) => e.to_string(), - _ => "error".to_string(), - } - } - - #[tokio::test] - pub async fn same_version() { - let result = check_version("0.0.0", "0.0.0").await; - let expected = "No updates found".to_string(); - assert_eq!(result, expected) - } - - #[tokio::test] - pub async fn both_older() { - let result = check_version("0.0.0-snapshot", "0.0.0-snapshot").await; - let expected = "No updates found".to_string(); - assert_eq!(result, expected) - } - - #[tokio::test] - pub async fn both_newer_component_newest() { - let result = check_version("0.1.0", "0.0.3").await; - let expected = warning("0.1.0"); - assert_eq!(result, expected) - } - - #[tokio::test] - pub async fn both_newer_worker_newest() { - let result = check_version("0.1.1", "0.2.0").await; - let expected = warning("0.2.0"); - assert_eq!(result, expected) - } - - #[tokio::test] - pub async fn newer_worker() { - let result = check_version("0.0.0", "0.0.1").await; - let expected = warning("0.0.1"); - assert_eq!(result, expected) - } - - #[tokio::test] - pub async fn newer_component() { - let result = check_version("0.0.1", "0.0.0").await; - let expected = warning("0.0.1"); - assert_eq!(result, expected) - } -} diff --git a/golem-cli/src/worker.rs b/golem-cli/src/worker.rs deleted file mode 100644 index 9614bba7a..000000000 --- a/golem-cli/src/worker.rs +++ /dev/null @@ -1,708 +0,0 @@ -// Copyright 2024 Golem Cloud -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::clients::component::ComponentClientLive; -use async_trait::async_trait; -use clap::builder::ValueParser; -use clap::Subcommand; -use golem_client::model::{ - Component, InvokeParameters, InvokeResult, ScanCursor, StringFilterComparator, Type, - WorkerFilter, WorkerMetadata, WorkerNameFilter, WorkersMetadataResponse, -}; -use golem_client::Context; -use golem_wasm_rpc::TypeAnnotatedValue; -use tokio::task::JoinHandle; -use tracing::{error, info}; -use uuid::Uuid; - -use crate::clients::worker::{WorkerClient, WorkerClientLive}; -use crate::component::{ComponentHandler, ComponentHandlerLive}; -use crate::model::component::function_params_types; -use crate::model::invoke_result_view::InvokeResultView; -use crate::model::text::WorkerAddView; -use crate::model::wave::type_to_analysed; -use crate::model::{ - ComponentId, ComponentIdOrName, Format, GolemError, GolemResult, IdempotencyKey, - JsonValueParser, WorkerName, WorkerUpdateMode, -}; -use crate::parse_key_val; - -#[derive(Subcommand, Debug)] -#[command()] -pub enum WorkerSubcommand { - /// Creates a new idle worker - #[command()] - Add { - /// The Golem component to use for the worker, identified by either its name or its component ID - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the newly created worker - #[arg(short, long)] - worker_name: WorkerName, - - /// List of environment variables (key-value pairs) passed to the worker - #[arg(short, long, value_parser = parse_key_val, value_name = "ENV=VAL")] - env: Vec<(String, String)>, - - /// List of command line arguments passed to the worker - #[arg(value_name = "args")] - args: Vec, - }, - - /// Generates an idempotency key for achieving at-most-one invocation when doing retries - #[command()] - IdempotencyKey {}, - - /// Invokes a worker and waits for its completion - #[command()] - InvokeAndAwait { - /// The Golem component the worker to be invoked belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - - /// A pre-generated idempotency key - #[arg(short = 'k', long)] - idempotency_key: Option, - - /// Name of the function to be invoked - #[arg(short, long)] - function: String, - - /// JSON array representing the parameters to be passed to the function - #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] - parameters: Option, - - /// Function parameter in WAVE format - /// - /// You can specify this argument multiple times for multiple parameters. - #[arg( - short = 'p', - long = "param", - value_name = "wave", - conflicts_with = "parameters" - )] - wave: Vec, - - /// Enables the STDIO cal;ing convention, passing the parameters through stdin instead of a typed exported interface - #[arg(short = 's', long, default_value_t = false)] - use_stdio: bool, - }, - - /// Triggers a function invocation on a worker without waiting for its completion - #[command()] - Invoke { - /// The Golem component the worker to be invoked belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - - /// A pre-generated idempotency key - #[arg(short = 'k', long)] - idempotency_key: Option, - - /// Name of the function to be invoked - #[arg(short, long)] - function: String, - - /// JSON array representing the parameters to be passed to the function - #[arg(short = 'j', long, value_name = "json", value_parser = ValueParser::new(JsonValueParser), conflicts_with = "wave")] - parameters: Option, - - /// Function parameter in WAVE format - /// - /// You can specify this argument multiple times for multiple parameters. - #[arg( - short = 'p', - long = "param", - value_name = "wave", - conflicts_with = "parameters" - )] - wave: Vec, - }, - - /// Connect to a worker and live stream its standard output, error and log channels - #[command()] - Connect { - /// The Golem component the worker to be connected to belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - }, - - /// Interrupts a running worker - #[command()] - Interrupt { - /// The Golem component the worker to be interrupted belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - }, - - /// Simulates a crash on a worker for testing purposes. - /// - /// The worker starts recovering and resuming immediately. - #[command()] - SimulatedCrash { - /// The Golem component the worker to be crashed belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - }, - - /// Deletes a worker - #[command()] - Delete { - /// The Golem component the worker to be deleted belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - }, - - /// Retrieves metadata about an existing worker - #[command()] - Get { - /// The Golem component the worker to be retrieved belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker - #[arg(short, long)] - worker_name: WorkerName, - }, - /// Retrieves metadata about an existing workers in a component - #[command()] - List { - /// The Golem component the workers to be retrieved belongs to - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Filter for worker metadata in form of `property op value`. - /// - /// Filter examples: `name = worker-name`, `version >= 0`, `status = Running`, `env.var1 = value`. - /// Can be used multiple times (AND condition is applied between them) - #[arg(short, long)] - filter: Option>, - - /// Position where to start listing, if not provided, starts from the beginning - /// - /// It is used to get the next page of results. To get next page, use the cursor returned in the response. - /// The cursor has the format 'layer/position' where both layer and position are numbers. - #[arg(short = 'P', long, value_parser = parse_cursor)] - cursor: Option, - - /// Count of listed values, if count is not provided, returns all values - #[arg(short = 'n', long)] - count: Option, - - /// Precision in relation to worker status, if true, calculate the most up-to-date status for each worker, default is false - #[arg(short, long)] - precise: Option, - }, - /// Updates a worker - #[command()] - Update { - /// The Golem component of the worker, identified by either its name or its component ID - #[command(flatten)] - component_id_or_name: ComponentIdOrName, - - /// Name of the worker to update - #[arg(short, long)] - worker_name: WorkerName, - - /// Update mode - auto or manual - #[arg(short, long)] - mode: WorkerUpdateMode, - - /// The new version of the updated worker - #[arg(short = 't', long)] - target_version: u64, - }, -} - -#[async_trait] -pub trait WorkerHandler { - async fn handle( - &self, - format: Format, - subcommand: WorkerSubcommand, - ) -> Result; -} - -pub struct WorkerHandlerLive<'r, C: WorkerClient + Send + Sync, R: ComponentHandler + Send + Sync> { - pub client: C, - pub components: &'r R, - pub worker_context: Context, - pub component_context: Context, - pub allow_insecure: bool, -} - -// same as resolve_worker_component_version, but with no borrowing, so we can spawn it. -async fn resolve_worker_component_version_no_ref( - worker_context: Context, - component_context: Context, - allow_insecure: bool, - component_id: ComponentId, - worker_name: WorkerName, -) -> Result, GolemError> { - let client = WorkerClientLive { - client: golem_client::api::WorkerClientLive { - context: worker_context.clone(), - }, - context: worker_context.clone(), - allow_insecure, - }; - - let components = ComponentHandlerLive { - client: ComponentClientLive { - client: golem_client::api::ComponentClientLive { - context: component_context.clone(), - }, - }, - }; - resolve_worker_component_version(&client, &components, &component_id, worker_name).await -} - -async fn resolve_worker_component_version< - C: WorkerClient + Send + Sync, - R: ComponentHandler + Send + Sync, ->( - client: &C, - components: &R, - component_id: &ComponentId, - worker_name: WorkerName, -) -> Result, GolemError> { - let worker_meta = client - .find_metadata( - component_id.clone(), - Some(WorkerFilter::Name(WorkerNameFilter { - comparator: StringFilterComparator::Equal, - value: worker_name.0, - })), - None, - Some(2), - Some(true), - ) - .await?; - - if worker_meta.workers.len() > 1 { - Err(GolemError( - "Multiple workers with the same name".to_string(), - )) - } else if let Some(worker) = worker_meta.workers.first() { - Ok(Some( - components - .get_metadata(component_id, worker.component_version) - .await?, - )) - } else { - Ok(None) - } -} - -fn wave_parameters_to_json( - wave: &[String], - component: &Component, - function: &str, -) -> Result { - let types = function_params_types(component, function)?; - - if wave.len() != types.len() { - return Err(GolemError(format!( - "Invalid number of wave parameters for function {function}. Expected {}, but got {}.", - types.len(), - wave.len() - ))); - } - - let params = wave - .iter() - .zip(types) - .map(|(param, typ)| parse_parameter(param, typ)) - .collect::, _>>()?; - - let json_params = params - .iter() - .map(golem_wasm_rpc::json::get_json_from_typed_value) - .collect::>(); - - Ok(serde_json::value::Value::Array(json_params)) -} - -fn parse_parameter(wave: &str, typ: &Type) -> Result { - match wasm_wave::from_str(&type_to_analysed(typ), wave) { - Ok(value) => Ok(value), - Err(err) => Err(GolemError(format!( - "Failed to parse wave parameter {wave}: {err:?}" - ))), - } -} - -async fn resolve_parameters( - client: &C, - components: &R, - component_id: &ComponentId, - worker_name: &WorkerName, - parameters: Option, - wave: Vec, - function: &str, -) -> Result<(serde_json::value::Value, Option), GolemError> { - if let Some(parameters) = parameters { - Ok((parameters, None)) - } else if let Some(component) = - resolve_worker_component_version(client, components, component_id, worker_name.clone()) - .await? - { - let json = wave_parameters_to_json(&wave, &component, function)?; - - Ok((json, Some(component))) - } else { - info!("No worker found with name {worker_name}. Assuming it should be create with the latest component version"); - let component = components.get_latest_metadata(component_id).await?; - - let json = wave_parameters_to_json(&wave, &component, function)?; - - // We are not going to use this component for result parsing. - Ok((json, None)) - } -} - -async fn to_invoke_result_view( - res: InvokeResult, - async_component_request: AsyncComponentRequest, - client: &C, - components: &R, - component_id: &ComponentId, - worker_name: &WorkerName, - function: &str, -) -> Result { - let component = match async_component_request { - AsyncComponentRequest::Empty => None, - AsyncComponentRequest::Resolved(component) => Some(component), - AsyncComponentRequest::Async(join_meta) => match join_meta.await.unwrap() { - Ok(Some(component)) => Some(component), - _ => None, - }, - }; - - let component = match component { - None => { - match resolve_worker_component_version( - client, - components, - component_id, - worker_name.clone(), - ) - .await - { - Ok(Some(component)) => component, - _ => { - error!("Failed to get worker metadata after successful call."); - - return Ok(InvokeResultView::Json(res.result)); - } - } - } - Some(component) => component, - }; - - Ok(InvokeResultView::try_parse_or_json( - res, &component, function, - )) -} - -enum AsyncComponentRequest { - Empty, - Resolved(Component), - Async(JoinHandle, GolemError>>), -} - -#[async_trait] -impl<'r, C: WorkerClient + Send + Sync, R: ComponentHandler + Send + Sync> WorkerHandler - for WorkerHandlerLive<'r, C, R> -{ - async fn handle( - &self, - format: Format, - subcommand: WorkerSubcommand, - ) -> Result { - match subcommand { - WorkerSubcommand::Add { - component_id_or_name, - worker_name, - env, - args, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - let inst = self - .client - .new_worker(worker_name, component_id, args, env) - .await?; - - Ok(GolemResult::Ok(Box::new(WorkerAddView(inst)))) - } - WorkerSubcommand::IdempotencyKey {} => { - let key = IdempotencyKey(Uuid::new_v4().to_string()); - Ok(GolemResult::Ok(Box::new(key))) - } - WorkerSubcommand::InvokeAndAwait { - component_id_or_name, - worker_name, - idempotency_key, - function, - parameters, - wave, - use_stdio, - } => { - let human_readable = format == Format::Text; - let component_id = self.components.resolve_id(component_id_or_name).await?; - - let (parameters, component_meta) = resolve_parameters( - &self.client, - self.components, - &component_id, - &worker_name, - parameters, - wave, - &function, - ) - .await?; - - let async_component_request = if let Some(component) = component_meta { - AsyncComponentRequest::Resolved(component) - } else if human_readable { - AsyncComponentRequest::Async(tokio::spawn( - resolve_worker_component_version_no_ref( - self.worker_context.clone(), - self.component_context.clone(), - self.allow_insecure, - component_id.clone(), - worker_name.clone(), - ), - )) - } else { - AsyncComponentRequest::Empty - }; - - let res = self - .client - .invoke_and_await( - worker_name.clone(), - component_id.clone(), - function.clone(), - InvokeParameters { params: parameters }, - idempotency_key, - use_stdio, - ) - .await?; - - if human_readable { - let view = to_invoke_result_view( - res, - async_component_request, - &self.client, - self.components, - &component_id, - &worker_name, - &function, - ) - .await?; - - Ok(GolemResult::Ok(Box::new(view))) - } else { - Ok(GolemResult::Json(res.result)) - } - } - WorkerSubcommand::Invoke { - component_id_or_name, - worker_name, - idempotency_key, - function, - parameters, - wave, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - let (parameters, _) = resolve_parameters( - &self.client, - self.components, - &component_id, - &worker_name, - parameters, - wave, - &function, - ) - .await?; - - self.client - .invoke( - worker_name, - component_id, - function, - InvokeParameters { params: parameters }, - idempotency_key, - ) - .await?; - - Ok(GolemResult::Str("Invoked".to_string())) - } - WorkerSubcommand::Connect { - component_id_or_name, - worker_name, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - self.client.connect(worker_name, component_id).await?; - - Err(GolemError("Unexpected connection closure".to_string())) - } - WorkerSubcommand::Interrupt { - component_id_or_name, - worker_name, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - self.client.interrupt(worker_name, component_id).await?; - - Ok(GolemResult::Str("Interrupted".to_string())) - } - WorkerSubcommand::SimulatedCrash { - component_id_or_name, - worker_name, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - self.client - .simulated_crash(worker_name, component_id) - .await?; - - Ok(GolemResult::Str("Done".to_string())) - } - WorkerSubcommand::Delete { - component_id_or_name, - worker_name, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - self.client.delete(worker_name, component_id).await?; - - Ok(GolemResult::Str("Deleted".to_string())) - } - WorkerSubcommand::Get { - component_id_or_name, - worker_name, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - let response = self.client.get_metadata(worker_name, component_id).await?; - - Ok(GolemResult::Ok(Box::new(response))) - } - WorkerSubcommand::List { - component_id_or_name, - filter, - count, - cursor, - precise, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - - if count.is_some() { - let response = self - .client - .list_metadata(component_id, filter, cursor, count, precise) - .await?; - - Ok(GolemResult::Ok(Box::new(response))) - } else { - let mut workers: Vec = vec![]; - let mut new_cursor = cursor; - - loop { - let response = self - .client - .list_metadata( - component_id.clone(), - filter.clone(), - new_cursor, - Some(50), - precise, - ) - .await?; - - workers.extend(response.workers); - - new_cursor = response.cursor; - - if new_cursor.is_none() { - break; - } - } - - Ok(GolemResult::Ok(Box::new(WorkersMetadataResponse { - workers, - cursor: None, - }))) - } - } - WorkerSubcommand::Update { - component_id_or_name, - worker_name, - target_version, - mode, - } => { - let component_id = self.components.resolve_id(component_id_or_name).await?; - let _ = self - .client - .update(worker_name, component_id, mode, target_version) - .await?; - - Ok(GolemResult::Str("Updated".to_string())) - } - } - } -} - -fn parse_cursor(s: &str) -> Result> { - let parts = s.split('/').collect::>(); - - if parts.len() != 2 { - return Err(format!("Invalid cursor format: {}", s).into()); - } - - Ok(ScanCursor { - layer: parts[0].parse()?, - cursor: parts[1].parse()?, - }) -}