use bytes::Bytes; use futures_util::Stream; use log::debug; use reqwest::Method; use std::collections::BTreeMap as Map; use std::fmt::Display; use std::net::IpAddr; pub struct Client { base_url: String, token: String, http_client: reqwest::Client, } impl Client { pub fn new(base_url: String, token: String) -> Self { Self { base_url, token, http_client: reqwest::Client::default(), } } pub fn with_proxy(self, proxy: String) -> reqwest::Result { let proxy = reqwest::Proxy::all(proxy)?; Ok(Self { http_client: reqwest::Client::builder().proxy(proxy).build()?, ..self }) } pub async fn clusters(&self) -> Result> { self.get_json("clusters").await } pub fn cluster(&self, name: String) -> Cluster<'_> { Cluster { dls: self, name } } pub async fn hosts(&self) -> Result> { self.get_json("hosts").await } pub fn host(&self, name: String) -> Host<'_> { Host { dls: self, name } } pub async fn sign_dl_set(&self, req: &DownloadSetReq) -> Result { let req = (self.req(Method::POST, "sign-download-set")?).json(req); self.req_json(req).await } pub async fn fetch_dl_set( &self, signed_dlset: &str, kind: &str, name: &str, asset: &str, ) -> Result>> { let req = self.get(format!( "public/download-set/{kind}/{name}/{asset}?set={signed_dlset}" ))?; let resp = do_req(req, &self.token).await?; Ok(resp.bytes_stream()) } pub async fn get_json(&self, path: impl Display) -> Result { self.req_json(self.get(&path)?).await } pub async fn get_bytes(&self, path: impl Display) -> Result> { let resp = do_req(self.get(&path)?, &self.token).await?; Ok(resp.bytes().await.map_err(Error::Read)?.to_vec()) } pub fn get(&self, path: impl Display) -> Result { self.req(Method::GET, path) } pub async fn req_json( &self, req: reqwest::RequestBuilder, ) -> Result { let req = req.header("Accept", "application/json"); let resp = do_req(req, &self.token).await?; let body = resp.bytes().await.map_err(Error::Read)?; serde_json::from_slice(&body).map_err(Error::Parse) } pub fn req(&self, method: Method, path: impl Display) -> Result { let uri = format!("{}/{path}", self.base_url); Ok((self.http_client.request(method, uri)) .header("Authorization", format!("Bearer {}", self.token))) } } pub struct Cluster<'t> { dls: &'t Client, name: String, } impl<'t> Cluster<'t> { pub async fn config(&self) -> Result { self.dls.get_json(format!("clusters/{}", self.name)).await } pub async fn ca_cert(&self, ca_name: &str) -> Result> { self.dls .get_bytes(format!("clusters/{}/CAs/{ca_name}/certificate", self.name)) .await } pub async fn token(&self, name: &str) -> Result { self.dls .get_json(format!("clusters/{}/tokens/{name}", self.name)) .await } pub async fn addons(&self) -> Result> { self.dls .get_bytes(format!("clusters/{}/addons", self.name)) .await } pub async fn ssh_userca_sign(&self, sign_req: &SshSignReq) -> Result> { let req = self.dls.req( Method::POST, format!("clusters/{}/ssh/user-ca/sign", self.name), )?; let req = req.json(sign_req); let resp = do_req(req, &self.dls.token).await?; Ok(resp.bytes().await.map_err(Error::Read)?.to_vec()) } pub async fn kube_sign(&self, sign_req: &KubeSignReq) -> Result> { let req = (self.dls).req(Method::POST, format!("clusters/{}/kube/sign", self.name))?; let req = req.json(sign_req); let resp = do_req(req, &self.dls.token).await?; Ok(resp.bytes().await.map_err(Error::Read)?.to_vec()) } } pub struct Host<'t> { dls: &'t Client, name: String, } impl<'t> Host<'t> { pub async fn config(&self) -> Result { self.dls.get_json(format!("hosts/{}", self.name)).await } pub async fn asset( &self, asset_name: &str, ) -> Result>> { let req = self.dls.get(format!("hosts/{}/{asset_name}", self.name))?; let resp = do_req(req, &self.dls.token).await?; Ok(resp.bytes_stream()) } } #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct Config { #[serde(default, deserialize_with = "deserialize_null_as_default")] pub clusters: Vec, #[serde(default, deserialize_with = "deserialize_null_as_default")] pub hosts: Vec, #[serde(default, deserialize_with = "deserialize_null_as_default")] pub host_templates: Vec, #[serde(default, rename = "SSLConfig")] pub ssl_config: String, } // compensate for go's encoder pitfalls use serde::{Deserialize, Deserializer}; fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result where T: Default + Deserialize<'de>, D: Deserializer<'de>, { let opt = Option::deserialize(deserializer)?; Ok(opt.unwrap_or_default()) } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct ClusterConfig { pub name: String, pub bootstrap_pods: String, pub addons: String, } #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct HostConfig { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub cluster_name: Option, #[serde(rename = "IPs")] pub ips: Vec, #[serde(default, skip_serializing_if = "Map::is_empty")] pub labels: Map, #[serde(default, skip_serializing_if = "Map::is_empty")] pub annotations: Map, #[serde(rename = "IPXE", skip_serializing_if = "Option::is_none")] pub ipxe: Option, pub initrd: String, pub kernel: String, pub versions: Map, /// initrd config template pub bootstrap_config: String, /// files to add to the final initrd config, with rendering #[serde(default, skip_serializing_if = "Vec::is_empty")] pub initrd_files: Vec, /// system config template pub config: String, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct SshSignReq { pub pub_key: String, pub principal: String, #[serde(skip_serializing_if = "Option::is_none")] pub validity: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub options: Vec, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct KubeSignReq { #[serde(rename = "CSR")] pub csr: String, pub user: String, #[serde(skip_serializing_if = "Option::is_none")] pub group: Option, #[serde(skip_serializing_if = "Option::is_none")] pub validity: Option, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct DownloadSetReq { pub expiry: String, #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, } #[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "PascalCase")] pub struct DownloadSetItem { pub kind: String, pub name: String, #[serde(skip_serializing_if = "Vec::is_empty")] pub assets: Vec, } #[derive(Debug, serde::Deserialize, serde::Serialize)] struct ServerError { #[serde(default)] code: u16, message: String, #[serde(skip_serializing_if = "Option::is_none")] details: Option, } pub async fn do_req(req: reqwest::RequestBuilder, token: &str) -> Result { let (client, req) = req.build_split(); let req = req.map_err(Error::Build)?; let method = req.method().clone(); let path = req.url().path().replace(token, ""); // clone required anyway, so replace the token as we copy debug!("request: {} {}", req.method(), req.url()); let resp = client.execute(req).await.map_err(Error::Send)?; let status = resp.status(); if !status.is_success() { let body = resp.bytes().await.map_err(Error::ErrorRead)?; let srv_err: ServerError = serde_json::from_slice(&body).map_err(|e| Error::ErrorParse { error: e, raw: body.to_vec(), })?; return Err(Error::ServerReject { method, path, status, message: srv_err.message, details: srv_err.details, }); } Ok(resp) } pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("request build failed: {0}")] Build(reqwest::Error), #[error("request send failed: {0}")] Send(reqwest::Error), #[error("response error read failed: {0}")] ErrorRead(reqwest::Error), #[error("response error parsing failed: {error}")] ErrorParse { error: serde_json::Error, raw: Vec, }, #[error("response read failed: {0}")] Read(reqwest::Error), #[error("rejected by server: {method} {path}: {} {message}", status.as_u16())] ServerReject { method: reqwest::Method, path: String, status: reqwest::StatusCode, message: String, details: Option, }, #[error("response parsing failed: {0}")] Parse(serde_json::Error), }