Split into several files and make more APIs
This commit is contained in:
parent
1922017831
commit
552fc7e5a0
8 changed files with 743 additions and 255 deletions
|
@ -15,6 +15,8 @@ serde = { version = "1.0.149", features = ["derive"] }
|
|||
log = "0.4"
|
||||
bytes = "1"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-manual-roots" ] }
|
||||
tokio = { version = "1.23", default-features = false, features = [ "macros" ] }
|
||||
futures = "0.3.25"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.22", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.23", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
|
206
examples/health-service-1.json
Normal file
206
examples/health-service-1.json
Normal file
|
@ -0,0 +1,206 @@
|
|||
[
|
||||
{
|
||||
"Node": {
|
||||
"ID": "9ccf821c-5e3e-cbe0-3727-0b0e5df35412",
|
||||
"Node": "celeri",
|
||||
"Address": "10.83.1.3",
|
||||
"Datacenter": "prod",
|
||||
"TaggedAddresses": {
|
||||
"lan": "10.83.1.3",
|
||||
"lan_ipv4": "10.83.1.3",
|
||||
"wan": "10.83.1.3",
|
||||
"wan_ipv4": "10.83.1.3"
|
||||
},
|
||||
"Meta": {
|
||||
"cname_target": "neptune.site.deuxfleurs.fr.",
|
||||
"consul-network-segment": "",
|
||||
"public_ipv4": "77.207.15.215",
|
||||
"public_ipv6": "2001:910:1204:1::33",
|
||||
"site": "neptune"
|
||||
},
|
||||
"CreateIndex": 28797576,
|
||||
"ModifyIndex": 28797584
|
||||
},
|
||||
"Service": {
|
||||
"ID": "_nomad-task-c7132dd2-cc89-cbb4-5e53-6bd7c8c33677-front-https-jitsi-https_port",
|
||||
"Service": "https-jitsi",
|
||||
"Tags": [
|
||||
"jitsi",
|
||||
"tricot jitsi.deuxfleurs.fr",
|
||||
"d53-cname jitsi.deuxfleurs.fr"
|
||||
],
|
||||
"Address": "10.83.1.3",
|
||||
"TaggedAddresses": {
|
||||
"lan_ipv4": {
|
||||
"Address": "10.83.1.3",
|
||||
"Port": 30140
|
||||
},
|
||||
"wan_ipv4": {
|
||||
"Address": "10.83.1.3",
|
||||
"Port": 30140
|
||||
}
|
||||
},
|
||||
"Meta": {
|
||||
"external-source": "nomad"
|
||||
},
|
||||
"Port": 30140,
|
||||
"Weights": {
|
||||
"Passing": 1,
|
||||
"Warning": 1
|
||||
},
|
||||
"EnableTagOverride": false,
|
||||
"Proxy": {
|
||||
"Mode": "",
|
||||
"MeshGateway": {},
|
||||
"Expose": {}
|
||||
},
|
||||
"Connect": {},
|
||||
"CreateIndex": 29922481,
|
||||
"ModifyIndex": 29922481
|
||||
},
|
||||
"Checks": [
|
||||
{
|
||||
"Node": "celeri",
|
||||
"CheckID": "serfHealth",
|
||||
"Name": "Serf Health Status",
|
||||
"Status": "critical",
|
||||
"Notes": "",
|
||||
"Output": "Agent not live or unreachable",
|
||||
"ServiceID": "",
|
||||
"ServiceName": "",
|
||||
"ServiceTags": [],
|
||||
"Type": "",
|
||||
"Interval": "",
|
||||
"Timeout": "",
|
||||
"ExposedPort": 0,
|
||||
"Definition": {},
|
||||
"CreateIndex": 28797576,
|
||||
"ModifyIndex": 32831331
|
||||
},
|
||||
{
|
||||
"Node": "celeri",
|
||||
"CheckID": "_nomad-check-1e8b81aa1790b51ffdf12117760d851bf00cb96f",
|
||||
"Name": "service: \"https-jitsi\" check",
|
||||
"Status": "passing",
|
||||
"Notes": "",
|
||||
"Output": "TCP connect 10.83.1.3:30140: Success",
|
||||
"ServiceID": "_nomad-task-c7132dd2-cc89-cbb4-5e53-6bd7c8c33677-front-https-jitsi-https_port",
|
||||
"ServiceName": "https-jitsi",
|
||||
"ServiceTags": [
|
||||
"jitsi",
|
||||
"tricot jitsi.deuxfleurs.fr",
|
||||
"d53-cname jitsi.deuxfleurs.fr"
|
||||
],
|
||||
"Type": "tcp",
|
||||
"Interval": "1m0s",
|
||||
"Timeout": "1m0s",
|
||||
"ExposedPort": 0,
|
||||
"Definition": {},
|
||||
"CreateIndex": 29922481,
|
||||
"ModifyIndex": 29922617
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Node": {
|
||||
"ID": "cee1c827-ff32-6f38-a815-69038a961a80",
|
||||
"Node": "dahlia",
|
||||
"Address": "10.83.2.1",
|
||||
"Datacenter": "prod",
|
||||
"TaggedAddresses": {
|
||||
"lan": "10.83.2.1",
|
||||
"lan_ipv4": "10.83.2.1",
|
||||
"wan": "10.83.2.1",
|
||||
"wan_ipv4": "10.83.2.1"
|
||||
},
|
||||
"Meta": {
|
||||
"cname_target": "orion.site.deuxfleurs.fr.",
|
||||
"consul-network-segment": "",
|
||||
"public_ipv4": "82.66.80.201",
|
||||
"public_ipv6": "2a01:e0a:28f:5e60::11",
|
||||
"site": "orion"
|
||||
},
|
||||
"CreateIndex": 31880680,
|
||||
"ModifyIndex": 31880870
|
||||
},
|
||||
"Service": {
|
||||
"ID": "_nomad-task-8214d936-aab2-35b4-d24e-8941a2d3b5b0-front-https-jitsi-https_port",
|
||||
"Service": "https-jitsi",
|
||||
"Tags": [
|
||||
"jitsi",
|
||||
"tricot jitsi.deuxfleurs.fr",
|
||||
"d53-cname jitsi.deuxfleurs.fr"
|
||||
],
|
||||
"Address": "10.83.2.1",
|
||||
"TaggedAddresses": {
|
||||
"lan_ipv4": {
|
||||
"Address": "10.83.2.1",
|
||||
"Port": 25753
|
||||
},
|
||||
"wan_ipv4": {
|
||||
"Address": "10.83.2.1",
|
||||
"Port": 25753
|
||||
}
|
||||
},
|
||||
"Meta": {
|
||||
"external-source": "nomad"
|
||||
},
|
||||
"Port": 25753,
|
||||
"Weights": {
|
||||
"Passing": 1,
|
||||
"Warning": 1
|
||||
},
|
||||
"EnableTagOverride": false,
|
||||
"Proxy": {
|
||||
"Mode": "",
|
||||
"MeshGateway": {},
|
||||
"Expose": {}
|
||||
},
|
||||
"Connect": {},
|
||||
"CreateIndex": 33002874,
|
||||
"ModifyIndex": 33002874
|
||||
},
|
||||
"Checks": [
|
||||
{
|
||||
"Node": "dahlia",
|
||||
"CheckID": "serfHealth",
|
||||
"Name": "Serf Health Status",
|
||||
"Status": "passing",
|
||||
"Notes": "",
|
||||
"Output": "Agent alive and reachable",
|
||||
"ServiceID": "",
|
||||
"ServiceName": "",
|
||||
"ServiceTags": [],
|
||||
"Type": "",
|
||||
"Interval": "",
|
||||
"Timeout": "",
|
||||
"ExposedPort": 0,
|
||||
"Definition": {},
|
||||
"CreateIndex": 31880680,
|
||||
"ModifyIndex": 31880680
|
||||
},
|
||||
{
|
||||
"Node": "dahlia",
|
||||
"CheckID": "_nomad-check-75542d8bde0dd9887ff8af10a02bf6a1f605695d",
|
||||
"Name": "service: \"https-jitsi\" check",
|
||||
"Status": "passing",
|
||||
"Notes": "",
|
||||
"Output": "TCP connect 10.83.2.1:25753: Success",
|
||||
"ServiceID": "_nomad-task-8214d936-aab2-35b4-d24e-8941a2d3b5b0-front-https-jitsi-https_port",
|
||||
"ServiceName": "https-jitsi",
|
||||
"ServiceTags": [
|
||||
"jitsi",
|
||||
"tricot jitsi.deuxfleurs.fr",
|
||||
"d53-cname jitsi.deuxfleurs.fr"
|
||||
],
|
||||
"Type": "tcp",
|
||||
"Interval": "1m0s",
|
||||
"Timeout": "1m0s",
|
||||
"ExposedPort": 0,
|
||||
"Definition": {},
|
||||
"CreateIndex": 33002874,
|
||||
"ModifyIndex": 33002958
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -13,12 +13,41 @@ async fn main() {
|
|||
let consul = Consul::new(config, "").unwrap();
|
||||
|
||||
println!("== LIST NODES ==");
|
||||
let list_nodes = consul.list_nodes().await.unwrap();
|
||||
println!("{:?}", list_nodes);
|
||||
let nodes = consul.catalog_node_list(None).await.unwrap();
|
||||
println!("{:?}", nodes);
|
||||
|
||||
println!("== CATALOG 1 ==");
|
||||
println!("{:?}", consul.watch_node("caribou", None).await.unwrap());
|
||||
|
||||
println!("== CATALOG 2 ==");
|
||||
println!("{:?}", consul.watch_node("cariacou", None).await.unwrap());
|
||||
if let Some(node) = nodes.first() {
|
||||
println!("== NODE {} ==", node.node);
|
||||
println!("{:?}", consul.catalog_node(&node.node, None).await.unwrap());
|
||||
}
|
||||
|
||||
println!("== LIST SERVICES ==");
|
||||
let services = consul.catalog_service_list(None).await.unwrap();
|
||||
println!("{:?}", services);
|
||||
|
||||
if let Some(service) = services.keys().next() {
|
||||
println!("== SERVICE NODES {} ==", service);
|
||||
println!(
|
||||
"{:?}",
|
||||
consul.catalog_service_nodes(service, None).await.unwrap()
|
||||
);
|
||||
|
||||
println!("== SERVICE HEALTH {} ==", service);
|
||||
println!(
|
||||
"{:?}",
|
||||
consul
|
||||
.health_service_instances(service, None)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
println!("== WATCHING EVERYTHING ==");
|
||||
let mut watch = consul.watch_all_service_health();
|
||||
loop {
|
||||
if watch.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
println!("\n{:?}", watch.borrow_and_update());
|
||||
}
|
||||
}
|
||||
|
|
230
src/catalog.rs
Normal file
230
src/catalog.rs
Normal file
|
@ -0,0 +1,230 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::select;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::{Consul, WithIndex};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulNode {
|
||||
pub node: String,
|
||||
pub address: String,
|
||||
pub meta: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulService {
|
||||
pub service: String,
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulCatalogNode {
|
||||
pub node: ConsulNode,
|
||||
pub services: HashMap<String, ConsulService>,
|
||||
}
|
||||
|
||||
pub type ConsulServiceList = HashMap<String, Vec<String>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulServiceNode {
|
||||
pub node: String,
|
||||
pub address: String,
|
||||
pub node_meta: HashMap<String, String>,
|
||||
pub service_name: String,
|
||||
pub service_tags: Vec<String>,
|
||||
pub service_address: String,
|
||||
pub service_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulHealthServiceNode {
|
||||
pub node: ConsulNode,
|
||||
pub service: ConsulService,
|
||||
pub checks: Vec<ConsulHealthCheck>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ConsulHealthCheck {
|
||||
pub node: String,
|
||||
#[serde(rename = "CheckID")]
|
||||
pub check_id: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub output: String,
|
||||
#[serde(rename = "Type")]
|
||||
pub type_: String,
|
||||
}
|
||||
|
||||
pub type AllServiceHealth = HashMap<String, Arc<[ConsulHealthServiceNode]>>;
|
||||
|
||||
impl Consul {
|
||||
pub async fn catalog_node_list(
|
||||
&self,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<Vec<ConsulNode>>> {
|
||||
self.get_with_index(format!("{}/v1/catalog/nodes", self.url), last_index)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn catalog_node(
|
||||
&self,
|
||||
host: &str,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<Option<ConsulCatalogNode>>> {
|
||||
self.get_with_index(format!("{}/v1/catalog/node/{}", self.url, host), last_index)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn catalog_service_list(
|
||||
&self,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<ConsulServiceList>> {
|
||||
self.get_with_index::<ConsulServiceList>(
|
||||
format!("{}/v1/catalog/services", self.url),
|
||||
last_index,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn catalog_service_nodes(
|
||||
&self,
|
||||
service: &str,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<Vec<ConsulServiceNode>>> {
|
||||
self.get_with_index(
|
||||
format!("{}/v1/catalog/service/{}", self.url, service),
|
||||
last_index,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn health_service_instances(
|
||||
&self,
|
||||
service: &str,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<Vec<ConsulHealthServiceNode>>> {
|
||||
self.get_with_index(
|
||||
format!("{}/v1/health/service/{}", self.url, service),
|
||||
last_index,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn watch_all_service_health(&self) -> watch::Receiver<AllServiceHealth> {
|
||||
let (tx, rx) = watch::channel(HashMap::new());
|
||||
|
||||
tokio::spawn(do_watch_all_service_health(self.clone(), tx));
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
async fn get_with_index<T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
mut url: String,
|
||||
last_index: Option<usize>,
|
||||
) -> Result<WithIndex<T>> {
|
||||
if let Some(i) = last_index {
|
||||
if url.contains('?') {
|
||||
write!(&mut url, "&index={}", i).unwrap();
|
||||
} else {
|
||||
write!(&mut url, "?index={}", i).unwrap();
|
||||
}
|
||||
}
|
||||
debug!("GET {} as {}", url, std::any::type_name::<T>());
|
||||
|
||||
let http = self.client.get(&url).send().await?;
|
||||
|
||||
Ok(WithIndex::<T>::index_from(&http)?.value(http.json().await?))
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_watch_all_service_health(consul: Consul, tx: watch::Sender<AllServiceHealth>) {
|
||||
let mut services = AllServiceHealth::new();
|
||||
let mut service_watchers = FuturesUnordered::<BoxFuture<(String, Result<_>)>>::new();
|
||||
let mut service_list: BoxFuture<Result<_>> = Box::pin(consul.catalog_service_list(None));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
list_res = &mut service_list => {
|
||||
match list_res {
|
||||
Ok(list) => {
|
||||
let list_index = list.index();
|
||||
for service in list.into_inner().keys() {
|
||||
if !services.contains_key(service) {
|
||||
services.insert(service.to_string(), Arc::new([]));
|
||||
|
||||
let service = service.to_string();
|
||||
service_watchers.push(Box::pin(async {
|
||||
let res = consul.health_service_instances(&service, None).await;
|
||||
(service, res)
|
||||
}));
|
||||
}
|
||||
}
|
||||
service_list = Box::pin(consul.catalog_service_list(Some(list_index)));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error listing services: {}", e);
|
||||
service_list = Box::pin(async {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
consul.catalog_service_list(None).await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
(service, watch_res) = service_watchers.next().then(some_or_pending) => {
|
||||
match watch_res {
|
||||
Ok(nodes) => {
|
||||
let index = nodes.index();
|
||||
services.insert(service.clone(), nodes.into_inner().into());
|
||||
|
||||
let consul = &consul;
|
||||
service_watchers.push(Box::pin(async move {
|
||||
let res = consul.health_service_instances(&service, Some(index)).await;
|
||||
(service, res)
|
||||
}));
|
||||
|
||||
if tx.send(services.clone()).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error getting service {}: {}", service, e);
|
||||
service_watchers.push(Box::pin(async {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
let res = consul.health_service_instances(&service, None).await;
|
||||
(service, res)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = tx.closed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn some_or_pending<T>(value: Option<T>) -> T {
|
||||
match value {
|
||||
Some(v) => v,
|
||||
None => futures::future::pending().await,
|
||||
}
|
||||
}
|
64
src/kv.rs
Normal file
64
src/kv.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use bytes::Bytes;
|
||||
use log::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Consul;
|
||||
|
||||
impl Consul {
|
||||
pub async fn kv_get(&self, key: &str) -> Result<Option<Bytes>> {
|
||||
debug!("kv_get {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}?raw", self.url, self.kv_prefix, key);
|
||||
let http = self.client.get(&url).send().await?;
|
||||
match http.status() {
|
||||
StatusCode::OK => Ok(Some(http.bytes().await?)),
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => Err(anyhow!(
|
||||
"Consul request failed: {:?}",
|
||||
http.error_for_status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kv_get_json<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
|
||||
debug!("kv_get_json {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}?raw", self.url, self.kv_prefix, key);
|
||||
let http = self.client.get(&url).send().await?;
|
||||
match http.status() {
|
||||
StatusCode::OK => Ok(Some(http.json().await?)),
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => Err(anyhow!(
|
||||
"Consul request failed: {:?}",
|
||||
http.error_for_status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kv_put(&self, key: &str, bytes: Bytes) -> Result<()> {
|
||||
debug!("kv_put {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kv_put_json<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
|
||||
debug!("kv_put_json {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.put(&url).json(value).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kv_delete(&self, key: &str) -> Result<()> {
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.delete(&url).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
199
src/lib.rs
199
src/lib.rs
|
@ -1,12 +1,14 @@
|
|||
use std::collections::HashMap;
|
||||
mod catalog;
|
||||
mod kv;
|
||||
mod locking;
|
||||
mod with_index;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use bytes::Bytes;
|
||||
use log::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub use with_index::WithIndex;
|
||||
|
||||
pub struct ConsulConfig {
|
||||
pub addr: String,
|
||||
|
@ -16,67 +18,6 @@ pub struct ConsulConfig {
|
|||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
// ---- Watch and retrieve Consul catalog ----
|
||||
//
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulNode {
|
||||
#[serde(rename = "Node")]
|
||||
pub node: String,
|
||||
#[serde(rename = "Address")]
|
||||
pub address: String,
|
||||
#[serde(rename = "Meta")]
|
||||
pub meta: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulServiceEntry {
|
||||
#[serde(rename = "Service")]
|
||||
pub service: String,
|
||||
|
||||
#[serde(rename = "Address")]
|
||||
pub address: String,
|
||||
|
||||
#[serde(rename = "Port")]
|
||||
pub port: u16,
|
||||
|
||||
#[serde(rename = "Tags")]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulNodeCatalog {
|
||||
#[serde(rename = "Node")]
|
||||
pub node: ConsulNode,
|
||||
#[serde(rename = "Services")]
|
||||
pub services: HashMap<String, ConsulServiceEntry>,
|
||||
}
|
||||
|
||||
// ---- Consul session management ----
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulSessionRequest {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "Node")]
|
||||
pub node: Option<String>,
|
||||
|
||||
#[serde(rename = "LockDelay")]
|
||||
pub lock_delay: Option<String>,
|
||||
|
||||
#[serde(rename = "TTL")]
|
||||
pub ttl: Option<String>,
|
||||
|
||||
#[serde(rename = "Behavior")]
|
||||
pub behavior: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulSessionResponse {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Consul {
|
||||
client: reqwest::Client,
|
||||
|
@ -131,128 +72,4 @@ impl Consul {
|
|||
kv_prefix: kv_prefix.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_nodes(&self) -> Result<Vec<ConsulNode>> {
|
||||
debug!("list_nodes");
|
||||
|
||||
let url = format!("{}/v1/catalog/nodes", self.url);
|
||||
|
||||
let http = self.client.get(&url).send().await?;
|
||||
let resp: Vec<ConsulNode> = http.json().await?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn watch_node(
|
||||
&self,
|
||||
host: &str,
|
||||
idx: Option<usize>,
|
||||
) -> Result<(Option<ConsulNodeCatalog>, usize)> {
|
||||
debug!("watch_node {} {:?}", host, idx);
|
||||
|
||||
let url = match idx {
|
||||
Some(i) => format!("{}/v1/catalog/node/{}?index={}", self.url, host, i),
|
||||
None => format!("{}/v1/catalog/node/{}", self.url, host),
|
||||
};
|
||||
|
||||
let http = self.client.get(&url).send().await?;
|
||||
let new_idx = match http.headers().get("X-Consul-Index") {
|
||||
Some(v) => v.to_str()?.parse::<usize>()?,
|
||||
None => bail!("X-Consul-Index header not found"),
|
||||
};
|
||||
|
||||
let resp: Option<ConsulNodeCatalog> = http.json().await?;
|
||||
Ok((resp, new_idx))
|
||||
}
|
||||
|
||||
// ---- KV get and put ----
|
||||
|
||||
pub async fn kv_get(&self, key: &str) -> Result<Option<Bytes>> {
|
||||
debug!("kv_get {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}?raw", self.url, self.kv_prefix, key);
|
||||
let http = self.client.get(&url).send().await?;
|
||||
match http.status() {
|
||||
StatusCode::OK => Ok(Some(http.bytes().await?)),
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => Err(anyhow!(
|
||||
"Consul request failed: {:?}",
|
||||
http.error_for_status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kv_get_json<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
|
||||
debug!("kv_get_json {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}?raw", self.url, self.kv_prefix, key);
|
||||
let http = self.client.get(&url).send().await?;
|
||||
match http.status() {
|
||||
StatusCode::OK => Ok(Some(http.json().await?)),
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => Err(anyhow!(
|
||||
"Consul request failed: {:?}",
|
||||
http.error_for_status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kv_put(&self, key: &str, bytes: Bytes) -> Result<()> {
|
||||
debug!("kv_put {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kv_put_json<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
|
||||
debug!("kv_put_json {}", key);
|
||||
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.put(&url).json(value).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kv_delete(&self, key: &str) -> Result<()> {
|
||||
let url = format!("{}/v1/kv/{}{}", self.url, self.kv_prefix, key);
|
||||
let http = self.client.delete(&url).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- Locking ----
|
||||
|
||||
pub async fn create_session(&self, req: &ConsulSessionRequest) -> Result<String> {
|
||||
debug!("create_session {:?}", req);
|
||||
|
||||
let url = format!("{}/v1/session/create", self.url);
|
||||
let http = self.client.put(&url).json(req).send().await?;
|
||||
let resp: ConsulSessionResponse = http.json().await?;
|
||||
Ok(resp.id)
|
||||
}
|
||||
|
||||
pub async fn acquire(&self, key: &str, bytes: Bytes, session: &str) -> Result<bool> {
|
||||
debug!("acquire {}", key);
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/kv/{}{}?acquire={}",
|
||||
self.url, self.kv_prefix, key, session
|
||||
);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
let resp: bool = http.json().await?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn release(&self, key: &str, bytes: Bytes, session: &str) -> Result<()> {
|
||||
debug!("release {}", key);
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/kv/{}{}?release={}",
|
||||
self.url, self.kv_prefix, key, session
|
||||
);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
65
src/locking.rs
Normal file
65
src/locking.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use anyhow::Result;
|
||||
use bytes::Bytes;
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Consul;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulSessionRequest {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "Node")]
|
||||
pub node: Option<String>,
|
||||
|
||||
#[serde(rename = "LockDelay")]
|
||||
pub lock_delay: Option<String>,
|
||||
|
||||
#[serde(rename = "TTL")]
|
||||
pub ttl: Option<String>,
|
||||
|
||||
#[serde(rename = "Behavior")]
|
||||
pub behavior: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsulSessionResponse {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl Consul {
|
||||
pub async fn create_session(&self, req: &ConsulSessionRequest) -> Result<String> {
|
||||
debug!("create_session {:?}", req);
|
||||
|
||||
let url = format!("{}/v1/session/create", self.url);
|
||||
let http = self.client.put(&url).json(req).send().await?;
|
||||
let resp: ConsulSessionResponse = http.json().await?;
|
||||
Ok(resp.id)
|
||||
}
|
||||
|
||||
pub async fn acquire(&self, key: &str, bytes: Bytes, session: &str) -> Result<bool> {
|
||||
debug!("acquire {}", key);
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/kv/{}{}?acquire={}",
|
||||
self.url, self.kv_prefix, key, session
|
||||
);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
let resp: bool = http.json().await?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn release(&self, key: &str, bytes: Bytes, session: &str) -> Result<()> {
|
||||
debug!("release {}", key);
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/kv/{}{}?release={}",
|
||||
self.url, self.kv_prefix, key, session
|
||||
);
|
||||
let http = self.client.put(&url).body(bytes).send().await?;
|
||||
http.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
75
src/with_index.rs
Normal file
75
src/with_index.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::fmt::{Debug, Display};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::Response;
|
||||
|
||||
pub struct WithIndex<T> {
|
||||
value: T,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<T> WithIndex<T> {
|
||||
pub fn index_from(resp: &Response) -> Result<WithIndexBuilder<T>> {
|
||||
let index = match resp.headers().get("X-Consul-Index") {
|
||||
Some(v) => v.to_str()?.parse::<usize>()?,
|
||||
None => bail!("X-Consul-Index header not found"),
|
||||
};
|
||||
Ok(WithIndexBuilder {
|
||||
index,
|
||||
_phantom: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::convert::AsRef<T> for WithIndex<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::borrow::Borrow<T> for WithIndex<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for WithIndex<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for WithIndex<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
<T as Debug>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Display for WithIndex<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
<T as Display>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WithIndexBuilder<T> {
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<T> WithIndexBuilder<T> {
|
||||
pub fn value(self, value: T) -> WithIndex<T> {
|
||||
WithIndex {
|
||||
value,
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue