lmrc-cloudflare 0.3.6

Cloudflare API client library for the LMRC Stack - comprehensive DNS, zones, and cache management with automatic retry logic
Documentation

lmrc-cloudflare

Part of the LMRC Stack - Infrastructure-as-Code toolkit for building production-ready Rust applications

Crates.io Documentation License

A comprehensive, well-documented Rust client for the Cloudflare API, designed specifically for CI/CD and automation workflows.

Features

  • DNS Management: Create, read, update, and delete DNS records with full control
  • Zone Management: List and query zones (domains)
  • Cache Purging: Clear cache by various methods (all, URLs, tags, hosts, prefixes)
  • Builder Pattern: Ergonomic API with builder pattern for all operations
  • Custom Error Types: Detailed error handling for programmatic responses
  • Diff Output: See what changes will be made before applying them
  • Idempotent Operations: Safe to run multiple times (perfect for CI/CD)
  • Async/Await: Built on modern async Rust with tokio and reqwest
  • Well Documented: Comprehensive docs and examples for every feature

Installation

Add this to your Cargo.toml:

[dependencies]
lmrc-cloudflare = "0.2"
tokio = { version = "1", features = ["full"] }

Quick Start

use lmrc_cloudflare::{CloudflareClient, dns::RecordType};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client
    let client = CloudflareClient::builder()
        .api_token("your-api-token")
        .build()?;

    // Get zone ID
    let zone_id = client.zones().get_zone_id("example.com").await?;

    // Create a DNS record
    let record = client.dns()
        .create_record(&zone_id)
        .name("api.example.com")
        .record_type(RecordType::A)
        .content("192.0.2.1")
        .proxied(true)
        .send()
        .await?;

    println!("Created record: {}", record.id);

    Ok(())
}

Usage Examples

DNS Management

List DNS Records

// List all DNS records for a zone
let records = client.dns()
    .list_records(&zone_id)
    .send()
    .await?;

// Filter by type
let a_records = client.dns()
    .list_records(&zone_id)
    .record_type(RecordType::A)
    .send()
    .await?;

// Filter by name and type
let specific_record = client.dns()
    .list_records(&zone_id)
    .name("api.example.com")
    .record_type(RecordType::A)
    .send()
    .await?;

Create DNS Records

use lmrc_cloudflare::dns::RecordType;

// Create an A record
let record = client.dns()
    .create_record(&zone_id)
    .name("api.example.com")
    .record_type(RecordType::A)
    .content("192.0.2.1")
    .proxied(true)
    .ttl(1)
    .comment("API server")
    .send()
    .await?;

// Create a CNAME record
let record = client.dns()
    .create_record(&zone_id)
    .name("www.example.com")
    .record_type(RecordType::CNAME)
    .content("example.com")
    .proxied(true)
    .send()
    .await?;

Update DNS Records

// Update record content
let updated = client.dns()
    .update_record(&zone_id, &record_id)
    .content("192.0.2.2")
    .send()
    .await?;

// Update multiple fields
let updated = client.dns()
    .update_record(&zone_id, &record_id)
    .content("192.0.2.3")
    .proxied(false)
    .ttl(3600)
    .send()
    .await?;

Delete DNS Records

client.dns()
    .delete_record(&zone_id, &record_id)
    .await?;

Find Records

// Find a record by name and type
let record = client.dns()
    .find_record(&zone_id, "api.example.com", RecordType::A)
    .await?;

if let Some(record) = record {
    println!("Found: {} -> {}", record.name, record.content);
}

Sync Records (Idempotent CI/CD Operations)

Perfect for CI/CD pipelines where you want to ensure DNS records match a desired state:

use lmrc_cloudflare::dns::{RecordType, DnsRecordBuilder};

// Define desired state
let desired_records = vec![
    DnsRecordBuilder::new()
        .name("api.example.com")
        .record_type(RecordType::A)
        .content("192.0.2.1")
        .proxied(true),
    DnsRecordBuilder::new()
        .name("www.example.com")
        .record_type(RecordType::CNAME)
        .content("example.com")
        .proxied(true),
];

// Dry run - see what would change without applying
let changes = client.dns()
    .sync_records(&zone_id)
    .records(desired_records.clone())
    .dry_run(true)
    .send()
    .await?;

for change in &changes {
    match change.action {
        ChangeAction::Create => println!("Would create: {}", change.description),
        ChangeAction::Update => println!("Would update: {}", change.description),
        ChangeAction::NoChange => println!("Already correct: {}", change.description),
        _ => {}
    }
}

// Apply changes
let changes = client.dns()
    .sync_records(&zone_id)
    .records(desired_records)
    .dry_run(false)
    .send()
    .await?;

println!("Applied {} changes", changes.iter().filter(|c| c.action.is_mutating()).count());

Zone Management

// List all zones
let zones = client.zones()
    .list()
    .send()
    .await?;

for zone in zones {
    println!("{}: {}", zone.name, zone.id);
}

// Get a specific zone
let zone = client.zones()
    .get(&zone_id)
    .await?;

// Find zone by name
let zone = client.zones()
    .find_by_name("example.com")
    .await?;

// Get zone ID by name (convenience method)
let zone_id = client.zones()
    .get_zone_id("example.com")
    .await?;

Cache Purging

// Purge everything (careful!)
client.cache()
    .purge_everything(&zone_id)
    .await?;

// Purge specific URLs
client.cache()
    .purge_urls(&zone_id)
    .urls(vec![
        "https://example.com/page1",
        "https://example.com/page2",
    ])
    .send()
    .await?;

// Purge by cache tags (Enterprise plan)
client.cache()
    .purge_tags(&zone_id)
    .tags(vec!["product", "blog"])
    .send()
    .await?;

// Purge by hosts (Enterprise plan)
client.cache()
    .purge_hosts(&zone_id)
    .hosts(vec!["www.example.com", "api.example.com"])
    .send()
    .await?;

// Purge by prefixes (Enterprise plan)
client.cache()
    .purge_prefixes(&zone_id)
    .prefixes(vec!["example.com/images/", "example.com/videos/"])
    .send()
    .await?;

Error Handling

The library provides detailed error types for better error handling:

use lmrc_cloudflare::Error;

match client.zones().get_zone_id("example.com").await {
    Ok(zone_id) => println!("Zone ID: {}", zone_id),
    Err(Error::NotFound(msg)) => eprintln!("Zone not found: {}", msg),
    Err(Error::Unauthorized(msg)) => eprintln!("Authentication failed: {}", msg),
    Err(Error::RateLimited { retry_after }) => {
        eprintln!("Rate limited, retry after: {:?} seconds", retry_after);
    }
    Err(Error::Api(api_error)) => {
        eprintln!("API error: {} (code: {:?})", api_error.message, api_error.code);
    }
    Err(e) => eprintln!("Error: {}", e),
}

CI/CD Integration Examples

GitHub Actions

name: Update DNS
on:
  push:
    branches: [main]

jobs:
  update-dns:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Update DNS records
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: cargo run --example sync-dns

Example Sync Script

use lmrc_cloudflare::{CloudflareClient, dns::{RecordType, DnsRecordBuilder}};
use std::env;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_token = env::var("CLOUDFLARE_API_TOKEN")?;
    let client = CloudflareClient::new(api_token)?;

    let zone_id = client.zones().get_zone_id("example.com").await?;

    let records = vec![
        DnsRecordBuilder::new()
            .name("api.example.com")
            .record_type(RecordType::A)
            .content(env::var("API_SERVER_IP")?)
            .proxied(true),
    ];

    let changes = client.dns()
        .sync_records(&zone_id)
        .records(records)
        .dry_run(false)
        .send()
        .await?;

    for change in changes {
        println!("{:?}: {}", change.action, change.description);
    }

    Ok(())
}

Authentication

The client supports Cloudflare API tokens. To create an API token:

  1. Go to Cloudflare Dashboard
  2. Click "Create Token"
  3. Select appropriate permissions (e.g., "Edit zone DNS" for DNS management)
  4. Copy the token and use it with the client
let client = CloudflareClient::builder()
    .api_token("your-api-token")
    .build()?;

Supported Record Types

  • A - IPv4 address
  • AAAA - IPv6 address
  • CNAME - Canonical name
  • MX - Mail exchange
  • TXT - Text record
  • SRV - Service locator
  • NS - Name server
  • CAA - Certificate authority authorization
  • PTR - Pointer record

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

Part of the LMRC Stack project. Licensed under either of:

at your option.

Acknowledgments

This client is not officially associated with Cloudflare, Inc.