Documentate

This commit is contained in:
Alex 2023-02-02 15:47:20 +01:00
parent db1d4411a9
commit d0f40c02b9
6 changed files with 95 additions and 38 deletions

View file

@ -2,7 +2,7 @@
name = "df-consul" name = "df-consul"
description = "Deuxfleurs' async Rust bindings for (a subset of) the Consul HTTP API" description = "Deuxfleurs' async Rust bindings for (a subset of) the Consul HTTP API"
authors = [ "Alex Auvolat <alex@adnab.me>" ] authors = [ "Alex Auvolat <alex@adnab.me>" ]
version = "0.3.0" version = "0.3.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/df-consul" repository = "https://git.deuxfleurs.fr/Deuxfleurs/df-consul"

View file

@ -2,7 +2,7 @@ use df_consul::*;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let config = ConsulConfig { let config = Config {
addr: "http://localhost:8500".into(), addr: "http://localhost:8500".into(),
ca_cert: None, ca_cert: None,
tls_skip_verify: false, tls_skip_verify: false,

View file

@ -1,3 +1,8 @@
//! Contains structures to interact with the catalog API
//!
//! See <https://developer.hashicorp.com/consul/api-docs/catalog>
//! for the full definition of the API.
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
@ -14,35 +19,44 @@ use tokio::sync::watch;
use crate::{Consul, WithIndex}; use crate::{Consul, WithIndex};
/// Node summary, as specified in response to "list nodes" API calls in
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes>
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulNode { pub struct Node {
pub node: String, pub node: String,
pub address: String, pub address: String,
pub meta: HashMap<String, String>, pub meta: HashMap<String, String>,
} }
/// One of the services returned in a CatalogNode
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulService { pub struct Service {
pub service: String, pub service: String,
pub address: String, pub address: String,
pub port: u16, pub port: u16,
pub tags: Vec<String>, pub tags: Vec<String>,
} }
/// Full node info, as specified in response to "retrieve map of services for a node" API call in
/// <https://developer.hashicorp.com/consul/api-docs/catalog#retrieve-map-of-services-for-a-node>
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulCatalogNode { pub struct CatalogNode {
pub node: ConsulNode, pub node: Node,
pub services: HashMap<String, ConsulService>, pub services: HashMap<String, Service>,
} }
pub type ConsulServiceList = HashMap<String, Vec<String>>; /// Concise service list, as specified in response to "list services" API call in
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-services>
pub type ServiceList = HashMap<String, Vec<String>>;
/// Node serving a service, as specified in response to "list nodes for a service" API call in
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes-for-service>
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulServiceNode { pub struct ServiceNode {
pub node: String, pub node: String,
pub address: String, pub address: String,
pub node_meta: HashMap<String, String>, pub node_meta: HashMap<String, String>,
@ -52,17 +66,21 @@ pub struct ConsulServiceNode {
pub service_port: u16, pub service_port: u16,
} }
/// Node serving a service with health info,
/// as specified in response to "list service instances for a service" health API call in
/// <https://developer.hashicorp.com/consul/api-docs/health#list-service-instances-for-service>
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulHealthServiceNode { pub struct HealthServiceNode {
pub node: ConsulNode, pub node: Node,
pub service: ConsulService, pub service: Service,
pub checks: Vec<ConsulHealthCheck>, pub checks: Vec<HealthCheck>,
} }
/// A health check as returned in HealthServiceNode
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct ConsulHealthCheck { pub struct HealthCheck {
pub node: String, pub node: String,
#[serde(rename = "CheckID")] #[serde(rename = "CheckID")]
pub check_id: String, pub check_id: String,
@ -73,42 +91,56 @@ pub struct ConsulHealthCheck {
pub type_: String, pub type_: String,
} }
pub type AllServiceHealth = HashMap<String, Arc<[ConsulHealthServiceNode]>>; /// Map containing all services and their associated nodes, with health checks,
/// returned by `watch_all_service_health`
pub type AllServiceHealth = HashMap<String, Arc<[HealthServiceNode]>>;
impl Consul { impl Consul {
/// The "list nodes" API call of the Catalog API
///
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes>
pub async fn catalog_node_list( pub async fn catalog_node_list(
&self, &self,
last_index: Option<usize>, last_index: Option<usize>,
) -> Result<WithIndex<Vec<ConsulNode>>> { ) -> Result<WithIndex<Vec<Node>>> {
self.get_with_index(format!("{}/v1/catalog/nodes", self.url), last_index) self.get_with_index(format!("{}/v1/catalog/nodes", self.url), last_index)
.await .await
} }
/// The "retrieve map of services for a node" API call of the Catalog API
///
/// <https://developer.hashicorp.com/consul/api-docs/catalog#retrieve-map-of-services-for-a-node>
pub async fn catalog_node( pub async fn catalog_node(
&self, &self,
host: &str, host: &str,
last_index: Option<usize>, last_index: Option<usize>,
) -> Result<WithIndex<Option<ConsulCatalogNode>>> { ) -> Result<WithIndex<Option<CatalogNode>>> {
self.get_with_index(format!("{}/v1/catalog/node/{}", self.url, host), last_index) self.get_with_index(format!("{}/v1/catalog/node/{}", self.url, host), last_index)
.await .await
} }
/// The "list services" API call of the Catalog api
///
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-services>
pub async fn catalog_service_list( pub async fn catalog_service_list(
&self, &self,
last_index: Option<usize>, last_index: Option<usize>,
) -> Result<WithIndex<ConsulServiceList>> { ) -> Result<WithIndex<ServiceList>> {
self.get_with_index::<ConsulServiceList>( self.get_with_index::<ServiceList>(
format!("{}/v1/catalog/services", self.url), format!("{}/v1/catalog/services", self.url),
last_index, last_index,
) )
.await .await
} }
/// The "list nodes for a service" API call of the Catalog api
///
/// <https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes-for-service>
pub async fn catalog_service_nodes( pub async fn catalog_service_nodes(
&self, &self,
service: &str, service: &str,
last_index: Option<usize>, last_index: Option<usize>,
) -> Result<WithIndex<Vec<ConsulServiceNode>>> { ) -> Result<WithIndex<Vec<ServiceNode>>> {
self.get_with_index( self.get_with_index(
format!("{}/v1/catalog/service/{}", self.url, service), format!("{}/v1/catalog/service/{}", self.url, service),
last_index, last_index,
@ -116,11 +148,14 @@ impl Consul {
.await .await
} }
/// The "list service instances for a service" API call of the Health api
///
/// <https://developer.hashicorp.com/consul/api-docs/health#list-service-instances-for-service>
pub async fn health_service_instances( pub async fn health_service_instances(
&self, &self,
service: &str, service: &str,
last_index: Option<usize>, last_index: Option<usize>,
) -> Result<WithIndex<Vec<ConsulHealthServiceNode>>> { ) -> Result<WithIndex<Vec<HealthServiceNode>>> {
self.get_with_index( self.get_with_index(
format!("{}/v1/health/service/{}", self.url, service), format!("{}/v1/health/service/{}", self.url, service),
last_index, last_index,
@ -128,6 +163,9 @@ impl Consul {
.await .await
} }
/// Launches a background task that watches all services and the nodes that serve them,
/// and make that info available in a tokio watch channel.
/// The worker terminates when the channel is dropped.
pub fn watch_all_service_health(&self) -> watch::Receiver<AllServiceHealth> { pub fn watch_all_service_health(&self) -> watch::Receiver<AllServiceHealth> {
let (tx, rx) = watch::channel(HashMap::new()); let (tx, rx) = watch::channel(HashMap::new());

View file

@ -1,6 +1,6 @@
mod catalog; pub mod catalog;
pub mod locking;
mod kv; mod kv;
mod locking;
mod with_index; mod with_index;
use std::fs::File; use std::fs::File;
@ -10,14 +10,23 @@ use anyhow::{bail, Result};
pub use with_index::WithIndex; pub use with_index::WithIndex;
pub struct ConsulConfig { /// Configuration parameters to talk to a Consul server
pub struct Config {
/// HTTP address of the Consul server, with `http://` or `https://` prefix
pub addr: String, pub addr: String,
/// CA certificate of the Consul CA, when using TLS
pub ca_cert: Option<String>, pub ca_cert: Option<String>,
pub tls_skip_verify: bool, /// Client certificate for client auth when using TLS
pub client_cert: Option<String>, pub client_cert: Option<String>,
/// Client key for client auth when using TLS
pub client_key: Option<String>, pub client_key: Option<String>,
/// Skip verification of consul server TLS certificates
pub tls_skip_verify: bool,
} }
/// Client used to talk to a Consul server.
/// All calls to the key/value API are automatically prefixed with an arbitrary string
/// that is constructed at client creation.
#[derive(Clone)] #[derive(Clone)]
pub struct Consul { pub struct Consul {
client: reqwest::Client, client: reqwest::Client,
@ -27,7 +36,7 @@ pub struct Consul {
} }
impl Consul { impl Consul {
pub fn new(config: ConsulConfig, kv_prefix: &str) -> Result<Self> { pub fn new(config: Config, kv_prefix: &str) -> Result<Self> {
let client = match (&config.client_cert, &config.client_key) { let client = match (&config.client_cert, &config.client_key) {
(Some(client_cert), Some(client_key)) => { (Some(client_cert), Some(client_key)) => {
let mut client_cert_buf = vec![]; let mut client_cert_buf = vec![];

View file

@ -1,3 +1,8 @@
//! Contains structures to interact with the locks/sessions API
//!
//! See <https://developer.hashicorp.com/consul/api-docs/session>
//! for the full definition of the API.
use anyhow::Result; use anyhow::Result;
use bytes::Bytes; use bytes::Bytes;
use log::*; use log::*;
@ -5,37 +10,33 @@ use serde::{Deserialize, Serialize};
use crate::Consul; use crate::Consul;
/// Session creation request as specified in
/// <https://developer.hashicorp.com/consul/api-docs/session#create-session>
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConsulSessionRequest { #[serde(rename_all = "PascalCase")]
#[serde(rename = "Name")] pub struct SessionRequest {
pub name: String, pub name: String,
#[serde(rename = "Node")]
pub node: Option<String>, pub node: Option<String>,
#[serde(rename = "LockDelay")]
pub lock_delay: Option<String>, pub lock_delay: Option<String>,
#[serde(rename = "TTL")] #[serde(rename = "TTL")]
pub ttl: Option<String>, pub ttl: Option<String>,
#[serde(rename = "Behavior")]
pub behavior: Option<String>, pub behavior: Option<String>,
} }
/// (for internal use, mostly)
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConsulSessionResponse { pub struct SessionResponse {
#[serde(rename = "ID")] #[serde(rename = "ID")]
pub id: String, pub id: String,
} }
impl Consul { impl Consul {
pub async fn create_session(&self, req: &ConsulSessionRequest) -> Result<String> { pub async fn create_session(&self, req: &SessionRequest) -> Result<String> {
debug!("create_session {:?}", req); debug!("create_session {:?}", req);
let url = format!("{}/v1/session/create", self.url); let url = format!("{}/v1/session/create", self.url);
let http = self.client.put(&url).json(req).send().await?; let http = self.client.put(&url).json(req).send().await?;
let resp: ConsulSessionResponse = http.json().await?; let resp: SessionResponse = http.json().await?;
Ok(resp.id) Ok(resp.id)
} }

View file

@ -3,12 +3,16 @@ use std::fmt::{Debug, Display};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use reqwest::Response; use reqwest::Response;
/// Wraps the returned value of an [API call with blocking
/// possibility](https://developer.hashicorp.com/consul/api-docs/features/blocking) with the
/// returned Consul index
pub struct WithIndex<T> { pub struct WithIndex<T> {
value: T, value: T,
index: usize, index: usize,
} }
impl<T> WithIndex<T> { impl<T> WithIndex<T> {
/// (for internal use, mostly)
pub fn index_from(resp: &Response) -> Result<WithIndexBuilder<T>> { pub fn index_from(resp: &Response) -> Result<WithIndexBuilder<T>> {
let index = match resp.headers().get("X-Consul-Index") { let index = match resp.headers().get("X-Consul-Index") {
Some(v) => v.to_str()?.parse::<usize>()?, Some(v) => v.to_str()?.parse::<usize>()?,
@ -20,10 +24,13 @@ impl<T> WithIndex<T> {
}) })
} }
/// Returns the inner value, discarding the index
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.value self.value
} }
/// Returns the Consul index, to be used in future calls to the same API endpoint to make them
/// blocking
pub fn index(&self) -> usize { pub fn index(&self) -> usize {
self.index self.index
} }
@ -60,12 +67,14 @@ impl<T: Display> Display for WithIndex<T> {
} }
} }
/// (for internal use, mostly)
pub struct WithIndexBuilder<T> { pub struct WithIndexBuilder<T> {
_phantom: std::marker::PhantomData<T>, _phantom: std::marker::PhantomData<T>,
index: usize, index: usize,
} }
impl<T> WithIndexBuilder<T> { impl<T> WithIndexBuilder<T> {
/// (for internal use, mostly)
pub fn value(self, value: T) -> WithIndex<T> { pub fn value(self, value: T) -> WithIndex<T> {
WithIndex { WithIndex {
value, value,