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