batata-consul-client 0.0.2

Rust client for HashiCorp Consul or batata
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use serde::{Deserialize, Serialize};

use crate::client::HttpClient;
use crate::error::Result;
use crate::types::{CatalogService, Node, QueryMeta, QueryOptions, WriteMeta, WriteOptions};

/// Catalog node registration
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogRegistration {
    /// Node ID
    #[serde(rename = "ID")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Node name
    pub node: String,
    /// Node address
    pub address: String,
    /// Tagged addresses
    #[serde(default)]
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub tagged_addresses: HashMap<String, String>,
    /// Node metadata
    #[serde(default)]
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub node_meta: HashMap<String, String>,
    /// Datacenter
    #[serde(skip_serializing_if = "Option::is_none")]
    pub datacenter: Option<String>,
    /// Service to register
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service: Option<CatalogServiceRegistration>,
    /// Check to register
    #[serde(skip_serializing_if = "Option::is_none")]
    pub check: Option<CatalogCheckRegistration>,
    /// Skip node update
    #[serde(default)]
    pub skip_node_update: bool,
}

impl CatalogRegistration {
    pub fn new(node: &str, address: &str) -> Self {
        Self {
            id: None,
            node: node.to_string(),
            address: address.to_string(),
            tagged_addresses: HashMap::new(),
            node_meta: HashMap::new(),
            datacenter: None,
            service: None,
            check: None,
            skip_node_update: false,
        }
    }

    pub fn with_service(mut self, service: CatalogServiceRegistration) -> Self {
        self.service = Some(service);
        self
    }

    pub fn with_check(mut self, check: CatalogCheckRegistration) -> Self {
        self.check = Some(check);
        self
    }
}

/// Service registration for catalog
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogServiceRegistration {
    /// Service ID
    #[serde(rename = "ID")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Service name
    pub service: String,
    /// Service tags
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    /// Service address
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
    /// Service port
    #[serde(skip_serializing_if = "Option::is_none")]
    pub port: Option<u16>,
    /// Service metadata
    #[serde(default)]
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub meta: HashMap<String, String>,
}

impl CatalogServiceRegistration {
    pub fn new(name: &str) -> Self {
        Self {
            id: None,
            service: name.to_string(),
            tags: Vec::new(),
            address: None,
            port: None,
            meta: HashMap::new(),
        }
    }

    pub fn with_id(mut self, id: &str) -> Self {
        self.id = Some(id.to_string());
        self
    }

    pub fn with_address(mut self, address: &str) -> Self {
        self.address = Some(address.to_string());
        self
    }

    pub fn with_port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }
}

/// Check registration for catalog
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogCheckRegistration {
    /// Node name
    pub node: String,
    /// Check ID
    #[serde(rename = "CheckID")]
    pub check_id: String,
    /// Check name
    pub name: String,
    /// Check status
    pub status: String,
    /// Service ID
    #[serde(rename = "ServiceID")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_id: Option<String>,
    /// Notes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    /// Output
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output: Option<String>,
}

/// Catalog deregistration
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogDeregistration {
    /// Node name
    pub node: String,
    /// Datacenter
    #[serde(skip_serializing_if = "Option::is_none")]
    pub datacenter: Option<String>,
    /// Service ID to deregister
    #[serde(rename = "ServiceID")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_id: Option<String>,
    /// Check ID to deregister
    #[serde(rename = "CheckID")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub check_id: Option<String>,
}

impl CatalogDeregistration {
    /// Deregister a node
    pub fn node(name: &str) -> Self {
        Self {
            node: name.to_string(),
            datacenter: None,
            service_id: None,
            check_id: None,
        }
    }

    /// Deregister a service from a node
    pub fn service(node: &str, service_id: &str) -> Self {
        Self {
            node: node.to_string(),
            datacenter: None,
            service_id: Some(service_id.to_string()),
            check_id: None,
        }
    }

    /// Deregister a check from a node
    pub fn check(node: &str, check_id: &str) -> Self {
        Self {
            node: node.to_string(),
            datacenter: None,
            service_id: None,
            check_id: Some(check_id.to_string()),
        }
    }
}

/// Catalog node with services
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CatalogNode {
    /// Node information
    pub node: Node,
    /// Services on this node
    pub services: HashMap<String, CatalogService>,
}

/// Catalog API client
pub struct Catalog {
    client: Arc<HttpClient>,
}

impl Catalog {
    /// Create a new Catalog client
    pub fn new(client: Arc<HttpClient>) -> Self {
        Self { client }
    }

    /// Register a node, service, or check
    pub async fn register(&self, registration: &CatalogRegistration, opts: Option<&WriteOptions>) -> Result<(bool, WriteMeta)> {
        let mut builder = self.client.put("/v1/catalog/register").json(registration);

        if let Some(opts) = opts {
            builder = self.client.apply_write_options(builder, opts);
        }

        self.client.write_bool(builder).await
    }

    /// Deregister a node, service, or check
    pub async fn deregister(&self, deregistration: &CatalogDeregistration, opts: Option<&WriteOptions>) -> Result<(bool, WriteMeta)> {
        let mut builder = self.client.put("/v1/catalog/deregister").json(deregistration);

        if let Some(opts) = opts {
            builder = self.client.apply_write_options(builder, opts);
        }

        self.client.write_bool(builder).await
    }

    /// List all datacenters
    pub async fn datacenters(&self) -> Result<Vec<String>> {
        let builder = self.client.get("/v1/catalog/datacenters");
        self.client.execute_json(builder).await
    }

    /// List all nodes
    pub async fn nodes(&self, opts: Option<&QueryOptions>) -> Result<(Vec<Node>, QueryMeta)> {
        let mut builder = self.client.get("/v1/catalog/nodes");

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// Get a single node
    pub async fn node(&self, name: &str, opts: Option<&QueryOptions>) -> Result<(Option<CatalogNode>, QueryMeta)> {
        let path = format!("/v1/catalog/node/{}", name);
        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        let response = self.client.execute(builder).await?;
        let meta = QueryMeta::default();

        let status = response.status();
        if status.as_u16() == 404 {
            return Ok((None, meta));
        }

        if status.is_success() {
            let node: CatalogNode = response.json().await.map_err(crate::error::ConsulError::HttpError)?;
            Ok((Some(node), meta))
        } else {
            let text = response.text().await.unwrap_or_default();
            Err(crate::error::ConsulError::api_error(status.as_u16(), text))
        }
    }

    /// List all services
    pub async fn services(&self, opts: Option<&QueryOptions>) -> Result<(HashMap<String, Vec<String>>, QueryMeta)> {
        let mut builder = self.client.get("/v1/catalog/services");

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// List nodes providing a service
    pub async fn service(
        &self,
        service: &str,
        tag: Option<&str>,
        opts: Option<&QueryOptions>,
    ) -> Result<(Vec<CatalogService>, QueryMeta)> {
        let mut path = format!("/v1/catalog/service/{}", service);
        if let Some(t) = tag {
            path.push_str(&format!("?tag={}", t));
        }

        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// List services on a node
    pub async fn node_services(&self, node: &str, opts: Option<&QueryOptions>) -> Result<(Option<CatalogNode>, QueryMeta)> {
        self.node(node, opts).await
    }
}