# lmrc-cloudflare

> Part of the [LMRC Stack](https://gitlab.com/lemarco/lmrc-stack) - Infrastructure-as-Code toolkit for building production-ready Rust applications

[![Crates.io](https://img.shields.io/crates/v/lmrc-cloudflare.svg)](https://crates.io/crates/lmrc-cloudflare)
[![Documentation](https://docs.rs/lmrc-cloudflare/badge.svg)](https://docs.rs/lmrc-cloudflare)
[![License](https://img.shields.io/crates/l/lmrc-cloudflare.svg)](https://gitlab.com/lemarco/lmrc-stack#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`:

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

## Quick Start

```rust
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

```rust
// 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

```rust
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

```rust
// 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

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

#### Find Records

```rust
// 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:

```rust
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

```rust
// 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

```rust
// 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:

```rust
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

```yaml
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

```rust
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](https://dash.cloudflare.com/profile/api-tokens)
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

```rust
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:

- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](../../LICENSE-MIT) or <http://opensource.org/licenses/MIT>)

at your option.

## Acknowledgments

This client is not officially associated with Cloudflare, Inc.
