1use core::fmt;
2use std::{
3 collections::{BTreeMap, HashMap},
4 fmt::Display,
5 fs::OpenOptions,
6 io::BufReader,
7};
8
9use hyper::{client::HttpConnector, http::uri::InvalidUri};
10use hyper_rustls::HttpsConnector;
11use rustls::{Certificate, PrivateKey, RootCertStore};
12use serde::{de::DeserializeOwned, Deserialize, Serialize};
13use serde_with::{serde_as, NoneAsEmptyString};
14
15pub mod config;
16pub use config::Config;
17
18pub type ConsulResult<T> = std::result::Result<T, Error>;
19
20#[derive(Debug, Clone)]
21pub struct Client {
22 client: hyper::Client<HttpsConnector<HttpConnector>>,
23 addr: String,
24}
25
26impl Client {
27 pub fn new(config: Config) -> ConsulResult<Self> {
28 let scheme = if config.tls.is_some() {
29 "https"
30 } else {
31 "http"
32 };
33 let ctor = if let Some(tls) = config.tls {
34 let mut root_store = RootCertStore::empty();
36 let mut cacert_file = BufReader::new(
37 OpenOptions::new()
38 .read(true)
39 .open(&tls.ca_file)
40 .map_err(Error::TlsSetup)?,
41 );
42 for cacert in rustls_pemfile::certs(&mut cacert_file).map_err(Error::TlsSetup)? {
43 root_store.add(&Certificate(cacert))?;
44 }
45
46 let mut cert_file = BufReader::new(
47 OpenOptions::new()
48 .read(true)
49 .open(&tls.cert_file)
50 .map_err(Error::TlsSetup)?,
51 );
52 let certs = rustls_pemfile::certs(&mut cert_file)
53 .map_err(Error::TlsSetup)?
54 .into_iter()
55 .map(Certificate)
56 .collect();
57
58 let mut key_file = BufReader::new(
59 OpenOptions::new()
60 .read(true)
61 .open(&tls.key_file)
62 .map_err(Error::TlsSetup)?,
63 );
64 let key = rustls_pemfile::pkcs8_private_keys(&mut key_file)
65 .map_err(Error::TlsSetup)?
66 .into_iter()
67 .map(PrivateKey)
68 .next()
69 .expect("could not find tls key");
70
71 let tls_config = rustls::ClientConfig::builder()
72 .with_safe_defaults()
73 .with_root_certificates(root_store)
74 .with_single_cert(certs, key)?;
75
76 hyper_rustls::HttpsConnectorBuilder::new()
77 .with_tls_config(tls_config)
78 .https_or_http()
79 .enable_http2()
80 .build()
81 } else {
82 hyper_rustls::HttpsConnectorBuilder::new()
85 .with_native_roots()
86 .https_or_http()
87 .enable_http1()
88 .build()
89 };
90
91 Ok(Self {
92 client: hyper::Client::builder().build(ctor),
93 addr: format!("{}://{}", scheme, config.address),
94 })
95 }
96
97 pub async fn agent_services(&self) -> ConsulResult<HashMap<String, AgentService>> {
98 self.request("/v1/agent/services").await
99 }
100
101 pub async fn agent_checks(&self) -> ConsulResult<HashMap<String, AgentCheck>> {
102 self.request("/v1/agent/checks").await
103 }
104
105 async fn request<P: Display, T: DeserializeOwned>(&self, path: P) -> ConsulResult<T> {
106 let res = match self
107 .client
108 .get(format!("{}{}", &self.addr, &path).parse()?)
109 .await
110 {
111 Ok(res) => res,
112 Err(e) => {
113 return Err(e.into());
114 }
115 };
116
117 if res.status() != hyper::StatusCode::OK {
118 return Err(Error::BadStatusCode(res.status()));
119 }
120
121 let bytes = hyper::body::to_bytes(res.into_body()).await?;
122
123 Ok(serde_json::from_slice(&bytes)?)
124 }
125}
126
127#[derive(Debug, thiserror::Error)]
128pub enum Error {
129 #[error(transparent)]
130 Hyper(#[from] hyper::Error),
131 #[error("bad status code: {0}")]
132 BadStatusCode(hyper::StatusCode),
133 #[error(transparent)]
134 InvalidUri(#[from] InvalidUri),
135 #[error(transparent)]
136 Serde(#[from] serde_json::Error),
137 #[error("tls setup: {0}")]
138 TlsSetup(std::io::Error),
139 #[error(transparent)]
140 Rustls(#[from] rustls::Error),
141 #[error(transparent)]
142 Webpki(#[from] webpki::Error),
143}
144
145#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
146#[serde(rename_all(deserialize = "PascalCase"))]
147pub struct AgentService {
148 #[serde(rename(deserialize = "ID"))]
149 pub id: String,
150 #[serde(rename(deserialize = "Service"))]
151 pub name: String,
152 #[serde(default)]
153 pub tags: Vec<String>,
154 #[serde(default)]
155 pub meta: BTreeMap<String, String>,
156 pub port: u16,
157 pub address: String,
158}
159
160#[serde_as]
161#[derive(Debug, Serialize, Deserialize)]
162#[serde(rename_all(deserialize = "PascalCase"))]
163pub struct AgentCheck {
164 #[serde(rename(deserialize = "CheckID"))]
165 pub id: String,
166 pub name: String,
167 pub status: ConsulCheckStatus,
168 pub output: String,
169 #[serde(rename(deserialize = "ServiceID"))]
170 pub service_id: String,
171 pub service_name: String,
172 #[serde_as(as = "NoneAsEmptyString")]
173 pub notes: Option<String>,
174}
175
176#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum ConsulCheckStatus {
179 Passing,
180 Warning,
181 Critical,
182}
183
184impl ConsulCheckStatus {
185 pub fn as_str(&self) -> &'static str {
186 match self {
187 ConsulCheckStatus::Passing => "passing",
188 ConsulCheckStatus::Warning => "warning",
189 ConsulCheckStatus::Critical => "critical",
190 }
191 }
192}
193
194impl fmt::Display for ConsulCheckStatus {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 self.as_str().fmt(f)
197 }
198}
199
200impl Default for ConsulCheckStatus {
201 fn default() -> Self {
202 Self::Passing
203 }
204}