open·source logoopen·source
All crates
Open source · Rust · MIT

consulx

v1.0.0

Consul KV CLI, REPL & Rust client

A pure-HTTP Consul KV toolkit built from scratch with reqwest — an interactive REPL (like redis-cli, but for Consul), a lightweight HTTP-only client library, typed JSON prefix loading, and key/prefix watches via blocking queries. Clone the client into your Actix or Axum app state for dynamic config and feature flags.

$cargo add consulx
consulkvcliactixaxum
~/consulx
consulx
v1.0.0 · MIT · Rust Rust
$ cargo add consulx
Interactive REPL — like redis-cli, but for Consul
Pure HTTP client (reqwest + rustls) — no consulrs, no SDK
Typed JSON helpers: kv_get / put / list_json<T>
Key & prefix watches via Consul blocking queries
ACL token + datacenter support; Clone into app state

// documentation

consulx docs

Overview

consulx is a modern Rust toolkit for working with Consul KV. It pairs an interactive REPL with a lightweight, HTTP-only client library — everything implemented from scratch with reqwest (rustls-tls, so https:// endpoints work with no native OpenSSL), no SDKs.

  • Interactive REPL (like redis-cli, but for Consul) with reedline auto-completion
  • Raw KV ops, tree view, key/prefix watches, editor integration
  • Typed JSON helpers for config and feature flags
  • ACL token (X-Consul-Token) and datacenter (?dc=) support
  • The ConsulXClient is Clone (a reqwest::Client is an Arc inside) — drop it into web app state
Note — consulx ships no framework-specific integration types. "Actix / Axum support" means the client is Clone, so you store it directly in actix_web::web::Data<ConsulXClient> or an Axum State<ConsulXClient> — there are no provided extractors or middleware.

Installation

toml
[dependencies]
consulx = "1.0.0"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["rustls-tls", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Or install the CLI directly:

bash
cargo install consulx
consulx

# Point at a specific endpoint
CONSUL_HTTP_ADDR=http://127.0.0.1:8500 consulx

REPL Commands

Command words are case-insensitive. reedline auto-completes the command keywords (after 2 characters) but not keys or values. Ctrl+C and Ctrl+D exit cleanly.

CommandUsageNotes
getget <key>Raw value, or <nil> if absent
putput <key> <value>Preserves internal spaces; strips one surrounding quote pair
del / deletedel <key>Delete a key (alias: delete)
listlist [prefix]Prefix optional (default = whole KV store)
treetree [prefix]ASCII tree; prefix optional
get-jsonget-json <key>Pretty-prints JSON; warns + shows raw if not JSON
put-jsonput-json <key> <json>Validates + minifies; argument kept verbatim
editedit <key>Opens value in $EDITOR (default nano)
watchwatch <key>Watch a single key (Ctrl+C to stop)
watch-prefixwatch-prefix <prefix>Watch all keys under a prefix
help / ?helpShow commands (alias: ?)
exit / quitexitLeave the REPL
Note — edit writes the value to a temp file, opens $EDITOR (supports args, e.g. EDITOR="code -w"), and writes back only if the file changed and the editor exited zero — otherwise it aborts with no write.

Client API

ConsulXClient is Clone with public fields http (reqwest::Client), base (URL, trailing slash trimmed), and dc (datacenter). Three constructors:

rust
use consulx::ConsulXClient;

// Explicit URL; also reads CONSUL_HTTP_TOKEN + CONSUL_DATACENTER/CONSUL_DC
let c = ConsulXClient::new("http://127.0.0.1:8500")?;

// Explicit token and/or datacenter
let c = ConsulXClient::with_options("http://127.0.0.1:8500", Some(token), Some("dc1".into()))?;

// From CONSUL_HTTP_ADDR (default http://127.0.0.1:8500)
let c = ConsulXClient::from_env()?;
rust
// Raw KV operations
kv_get_raw(key)        -> Result<Option<String>>   // None on 404
kv_put(key, value)     -> Result<()>
kv_delete(key)         -> Result<()>
kv_list(prefix)        -> Result<Vec<String>>       // [] on 404; surfaces 403 (ACL) as error

// Typed JSON helpers
kv_get_json<T: DeserializeOwned>(key)   -> Result<Option<T>>
kv_put_json<T: Serialize>(key, &T)      -> Result<()>
kv_list_json<T: DeserializeOwned>(prefix) -> Result<Vec<(String, T)>>

Typed JSON Prefix Loading

Load many typed JSON configs under a prefix in one call — perfect for feature flags and dynamic config. It lists the prefix, then fetches each key (an N+1 pattern), skipping empty values.

rust
#[derive(Deserialize)]
struct FeatureFlag { enabled: bool }

let flags = consul.kv_list_json::<FeatureFlag>("app/features/").await?;

for (key, flag) in flags {
    println!("{key} => {:?}", flag);
}

Watches (Blocking Queries)

The library watch methods take the last-seen index and return the new index alongside the value, so you drive the blocking-query loop. Consul is polled with a fixed wait=10s; the new index comes from the X-Consul-Index response header.

rust
// returns (new_index, value)
kv_watch(key: &str, index: Option<u64>) -> Result<(u64, Option<String>)>

// returns (new_index, keys)
kv_watch_prefix(prefix: &str, index: Option<u64>) -> Result<(u64, Vec<String>)>

// typical loop
let mut idx = None;
loop {
    let (new_idx, value) = consul.kv_watch("app/config", idx).await?;
    if Some(new_idx) != idx { println!("changed: {:?}", value); }
    idx = Some(new_idx);
}
Note — The REPL watch / watch-prefix commands wrap this loop for you and handle Consul index-reset semantics (backing off when the index resets to 0).

Actix & Axum Usage

There are no bespoke integration types — you just clone the Clone-able client into your framework state and use it from handlers.

rust
// Actix
let consul = ConsulXClient::from_env()?;
App::new()
    .app_data(web::Data::new(consul.clone()))
    .route("/flag/{k}", web::get().to(get_flag));

async fn get_flag(consul: web::Data<ConsulXClient>, path: web::Path<String>) -> impl Responder {
    let v = consul.kv_get_raw(&path).await.unwrap_or(None);
    HttpResponse::Ok().body(v.unwrap_or_else(|| "<nil>".into()))
}

HTTP & Environment

All requests target {base}/v1/kv/{key}. Keys are percent-encoded but "/" is preserved as a path separator (keys are hierarchical). A configured datacenter is appended as ?dc= on every request; a token is sent as the X-Consul-Token header.

VariableDefaultPurpose
CONSUL_HTTP_ADDRhttp://127.0.0.1:8500Consul base URL (from_env)
CONSUL_HTTP_TOKENnoneACL token → X-Consul-Token header
CONSUL_DATACENTERnoneDatacenter → ?dc= query param
CONSUL_DCnoneFallback for the datacenter
EDITORnanoEditor for the edit command

Ready to try consulx?

cargo add consulx
View on crates.io

// more

consulx — help & policies