komodo_client/
lib.rs

1//! # Komodo
2//! *A system to build and deploy software across many servers*. [**https://komo.do**](https://komo.do)
3//!
4//! This is a client library for the Komodo Core API.
5//! It contains:
6//! - Definitions for the application [api] and [entities].
7//! - A [client][KomodoClient] to interact with the Komodo Core API.
8//! - Information on configuring Komodo [Core][entities::config::core] and [Periphery][entities::config::periphery].
9//!
10//! ## Client Configuration
11//!
12//! The client includes a convenenience method to parse the Komodo API url and credentials from the environment:
13//! - `KOMODO_ADDRESS`
14//! - `KOMODO_API_KEY`
15//! - `KOMODO_API_SECRET`
16//!
17//! ## Client Example
18//! ```
19//! dotenvy::dotenv().ok();
20//!
21//! let client = KomodoClient::new_from_env()?;
22//!
23//! // Get all the deployments
24//! let deployments = client.read(ListDeployments::default()).await?;
25//!
26//! println!("{deployments:#?}");
27//!
28//! let update = client.execute(RunBuild { build: "test-build".to_string() }).await?:
29//! ```
30
31use std::{sync::OnceLock, time::Duration};
32
33use anyhow::Context;
34use api::read::GetVersion;
35use serde::Deserialize;
36
37pub mod api;
38pub mod busy;
39pub mod deserializers;
40pub mod entities;
41pub mod parsers;
42pub mod terminal;
43pub mod ws;
44
45mod request;
46
47/// &'static KomodoClient initialized from environment.
48pub fn komodo_client() -> &'static KomodoClient {
49  static KOMODO_CLIENT: OnceLock<KomodoClient> = OnceLock::new();
50  KOMODO_CLIENT.get_or_init(|| {
51    KomodoClient::new_from_env()
52      .context("Missing KOMODO_ADDRESS, KOMODO_API_KEY, KOMODO_API_SECRET from env")
53      .unwrap()
54  })
55}
56
57/// Default environment variables for the [KomodoClient].
58#[derive(Deserialize)]
59pub struct KomodoEnv {
60  /// KOMODO_ADDRESS
61  pub komodo_address: String,
62  /// KOMODO_API_KEY
63  pub komodo_api_key: String,
64  /// KOMODO_API_SECRET
65  pub komodo_api_secret: String,
66}
67
68/// Client to interface with [Komodo](https://komo.do/docs/api#rust-client)
69#[derive(Clone)]
70pub struct KomodoClient {
71  #[cfg(not(feature = "blocking"))]
72  reqwest: reqwest::Client,
73  #[cfg(feature = "blocking")]
74  reqwest: reqwest::blocking::Client,
75  address: String,
76  key: String,
77  secret: String,
78}
79
80impl KomodoClient {
81  /// Initializes KomodoClient, including a health check.
82  pub fn new(
83    address: impl Into<String>,
84    key: impl Into<String>,
85    secret: impl Into<String>,
86  ) -> KomodoClient {
87    KomodoClient {
88      reqwest: Default::default(),
89      address: address.into(),
90      key: key.into(),
91      secret: secret.into(),
92    }
93  }
94
95  /// Initializes KomodoClient from environment: [KomodoEnv]
96  pub fn new_from_env() -> anyhow::Result<KomodoClient> {
97    let KomodoEnv {
98      komodo_address,
99      komodo_api_key,
100      komodo_api_secret,
101    } = envy::from_env()
102      .context("failed to parse environment for komodo client")?;
103    Ok(KomodoClient::new(
104      komodo_address,
105      komodo_api_key,
106      komodo_api_secret,
107    ))
108  }
109
110  /// Add a healthcheck in the initialization pipeline:
111  ///
112  /// ```rust
113  /// let komodo = KomodoClient::new_from_env()?
114  ///   .with_healthcheck().await?;
115  /// ```
116  #[cfg(not(feature = "blocking"))]
117  pub async fn with_healthcheck(self) -> anyhow::Result<Self> {
118    self.health_check().await?;
119    Ok(self)
120  }
121
122  /// Add a healthcheck in the initialization pipeline:
123  ///
124  /// ```rust
125  /// let komodo = KomodoClient::new_from_env()?
126  ///   .with_healthcheck().await?;
127  /// ```
128  #[cfg(feature = "blocking")]
129  pub fn with_healthcheck(self) -> anyhow::Result<Self> {
130    self.health_check()?;
131    Ok(self)
132  }
133
134  /// Get the Core version.
135  #[cfg(not(feature = "blocking"))]
136  pub async fn core_version(&self) -> anyhow::Result<String> {
137    self.read(GetVersion {}).await.map(|r| r.version)
138  }
139
140  /// Get the Core version.
141  #[cfg(feature = "blocking")]
142  pub fn core_version(&self) -> anyhow::Result<String> {
143    self.read(GetVersion {}).map(|r| r.version)
144  }
145
146  /// Send a health check.
147  #[cfg(not(feature = "blocking"))]
148  pub async fn health_check(&self) -> anyhow::Result<()> {
149    self.read(GetVersion {}).await.map(|_| ())
150  }
151
152  /// Send a health check.
153  #[cfg(feature = "blocking")]
154  pub fn health_check(&self) -> anyhow::Result<()> {
155    self.read(GetVersion {}).map(|_| ())
156  }
157
158  /// Use a custom reqwest client.
159  #[cfg(not(feature = "blocking"))]
160  pub fn set_reqwest(mut self, reqwest: reqwest::Client) -> Self {
161    self.reqwest = reqwest;
162    self
163  }
164
165  /// Use a custom reqwest client.
166  #[cfg(feature = "blocking")]
167  pub fn set_reqwest(
168    mut self,
169    reqwest: reqwest::blocking::Client,
170  ) -> Self {
171    self.reqwest = reqwest;
172    self
173  }
174
175  /// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the
176  /// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.
177  #[cfg(not(feature = "blocking"))]
178  pub async fn poll_update_until_complete(
179    &self,
180    update_id: impl Into<String>,
181  ) -> anyhow::Result<entities::update::Update> {
182    let update_id = update_id.into();
183    loop {
184      let update = self
185        .read(api::read::GetUpdate {
186          id: update_id.clone(),
187        })
188        .await?;
189      if update.status == entities::update::UpdateStatus::Complete {
190        return Ok(update);
191      }
192      tokio::time::sleep(Duration::from_millis(500)).await;
193    }
194  }
195
196  /// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the
197  /// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.
198  #[cfg(feature = "blocking")]
199  pub fn poll_update_until_complete(
200    &self,
201    update_id: impl Into<String>,
202  ) -> anyhow::Result<entities::update::Update> {
203    let update_id = update_id.into();
204    loop {
205      let update = self.read(api::read::GetUpdate {
206        id: update_id.clone(),
207      })?;
208      if update.status == entities::update::UpdateStatus::Complete {
209        return Ok(update);
210      }
211    }
212  }
213}