use crate::cli::commands::{
DnsCmd, DnsProviderKind, DnsRecordBatchCmd, DnsRecordCreateCmd, DnsRecordDeleteCmd,
DnsRecordEditCmd, DnsRecordExportCmd, DnsRecordGetCmd, DnsRecordImportCmd, DnsRecordListCmd,
DnsRecordReplaceCmd, DnsRecordsSubCommand, DnsSettingsCmd, DnsSettingsEditCmd,
DnsSettingsSubCommand, DnsSubCommand, DnsZoneCreateCmd, DnsZoneDeleteCmd, DnsZoneEditCmd,
DnsZoneGetCmd, DnsZoneListCmd, DnsZonesSubCommand, DnssecCmd, DnssecEditCmd, DnssecGetCmd,
DnssecSubCommand,
};
use crate::config::{resolve_cloudflare_account_id, resolve_cloudflare_api_token};
use crate::provider_support::cloudflare::{
CloudflareClient, CloudflareDnsRecordListFilters, CloudflareZoneAccountWrite,
CloudflareZoneCreateRequest, CloudflareZoneEditRequest,
};
use crate::provider_support::{
dns_providers, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
CloudflareDnsSettingsInternalDns, CloudflareDnsSettingsNameservers, CloudflareDnsSettingsSoa,
CloudflareDnssecEdit, CloudflareZoneFilters,
};
use serde_json::Value;
use std::fs;
pub async fn run_dns(cmd: DnsCmd, _debug: bool) -> Result<(), String> {
match cmd.command {
DnsSubCommand::Providers => {
for provider in dns_providers() {
println!(
"{}\t{:?}\t{}",
provider.key,
provider.status,
provider.notes.unwrap_or_default()
);
}
Ok(())
}
DnsSubCommand::Zones(zones) => match zones.command {
DnsZonesSubCommand::List(args) => list_zones(args).await,
DnsZonesSubCommand::Get(args) => get_zone(args).await,
DnsZonesSubCommand::Create(args) => create_zone(args).await,
DnsZonesSubCommand::Edit(args) => edit_zone(args).await,
DnsZonesSubCommand::Delete(args) => delete_zone(args).await,
},
DnsSubCommand::Records(records) => match records.command {
DnsRecordsSubCommand::List(args) => list_records(args).await,
DnsRecordsSubCommand::Get(args) => get_record(args).await,
DnsRecordsSubCommand::Create(args) => create_record(args).await,
DnsRecordsSubCommand::Replace(args) => replace_record(args).await,
DnsRecordsSubCommand::Edit(args) => edit_record(args).await,
DnsRecordsSubCommand::Delete(args) => delete_record(args).await,
DnsRecordsSubCommand::Batch(args) => batch_records(args).await,
DnsRecordsSubCommand::Import(args) => import_records(args).await,
DnsRecordsSubCommand::Export(args) => export_records(args).await,
},
DnsSubCommand::Dnssec(DnssecCmd { command }) => match command {
DnssecSubCommand::Get(args) => get_dnssec(args).await,
DnssecSubCommand::Edit(args) => edit_dnssec(args).await,
},
DnsSubCommand::Settings(DnsSettingsCmd { command }) => match command {
DnsSettingsSubCommand::Get(args) => get_settings(args).await,
DnsSettingsSubCommand::Edit(args) => edit_settings(args).await,
},
}
}
async fn list_zones(args: DnsZoneListCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, args.account_id.clone())?;
let (zones, page) = client
.list_zones(&CloudflareZoneFilters {
account_id: args.account_id,
account_name: args.account_name,
account_name_op: args.account_name_op,
name: args.name,
name_op: args.name_op,
status: args.status,
r#type: args.zone_types,
r#match: args.r#match,
order: args.order,
direction: args.direction,
page: args.page,
per_page: args.per_page,
})
.await?;
print_json(&serde_json::json!({
"provider": "cloudflare",
"zones": zones,
"result_info": page,
}))
}
async fn get_zone(args: DnsZoneGetCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(&client.get_zone(&args.zone_id).await?)
}
async fn create_zone(args: DnsZoneCreateCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, args.account_id.clone())?;
print_json(
&client
.create_zone(&CloudflareZoneCreateRequest {
name: args.name,
account: args
.account_id
.map(|id| CloudflareZoneAccountWrite { id: Some(id) }),
jump_start: Some(args.jump_start),
zone_type: args.zone_type,
})
.await?,
)
}
async fn edit_zone(args: DnsZoneEditCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(
&client
.edit_zone(
&args.zone_id,
&CloudflareZoneEditRequest {
paused: args.paused,
zone_type: args.zone_type,
vanity_name_servers: (!args.vanity_name_servers.is_empty())
.then_some(args.vanity_name_servers),
},
)
.await?,
)
}
async fn delete_zone(args: DnsZoneDeleteCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
client.delete_zone(&args.zone_id).await?;
println!("Deleted zone {}", args.zone_id);
Ok(())
}
async fn list_records(args: DnsRecordListCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
let (records, page) = client
.list_records(
&args.zone_id,
&CloudflareDnsRecordListFilters {
name: args.name,
record_type: args.record_type,
page: args.page,
per_page: args.per_page,
},
)
.await?;
print_json(&serde_json::json!({
"provider": "cloudflare",
"zone_id": args.zone_id,
"records": records,
"result_info": page
}))
}
async fn get_record(args: DnsRecordGetCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(&client.get_record(&args.zone_id, &args.record_id).await?)
}
async fn create_record(args: DnsRecordCreateCmd) -> Result<(), String> {
let zone_id = args.zone_id.clone();
let token = args.token.clone();
let provider = args.provider;
let request = record_write_from_create(args)?;
let client = cloudflare_client(provider, token, None)?;
print_json(&client.create_record(&zone_id, &request).await?)
}
async fn replace_record(args: DnsRecordReplaceCmd) -> Result<(), String> {
let zone_id = args.common.zone_id.clone();
let provider = args.common.provider;
let token = args.common.token.clone();
let request = record_write_from_create(args.common)?;
let client = cloudflare_client(provider, token, None)?;
print_json(
&client
.replace_record(&zone_id, &args.record_id, &request)
.await?,
)
}
async fn edit_record(args: DnsRecordEditCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(
&client
.edit_record(
&args.zone_id,
&args.record_id,
&CloudflareDnsRecordWrite {
record_type: args.record_type,
name: args.name,
content: args.content,
ttl: args.ttl,
proxied: args.proxied,
priority: args.priority,
comment: args.comment,
tags: (!args.tags.is_empty()).then_some(args.tags),
data: parse_optional_json(args.data_json.as_deref())?,
settings: parse_optional_json(args.settings_json.as_deref())?,
},
)
.await?,
)
}
async fn delete_record(args: DnsRecordDeleteCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
client.delete_record(&args.zone_id, &args.record_id).await?;
println!("Deleted record {}", args.record_id);
Ok(())
}
async fn batch_records(args: DnsRecordBatchCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
let payload = fs::read_to_string(&args.input)
.map_err(|error| format!("Failed to read {}: {}", args.input.display(), error))?;
let request: CloudflareDnsRecordBatch =
serde_json::from_str(&payload).map_err(|error| format!("Invalid batch JSON: {}", error))?;
print_json(&client.batch_records(&args.zone_id, &request).await?)
}
async fn import_records(args: DnsRecordImportCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
let bytes = fs::read(&args.file)
.map_err(|error| format!("Failed to read {}: {}", args.file.display(), error))?;
print_json(&client.import_records(&args.zone_id, bytes).await?)
}
async fn export_records(args: DnsRecordExportCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
let response = client.export_records(&args.zone_id).await?;
if let Some(output) = args.output {
fs::write(&output, &response.body)
.map_err(|error| format!("Failed to write {}: {}", output.display(), error))?;
println!("Wrote exported zone file to {}", output.display());
return Ok(());
}
println!("{}", String::from_utf8_lossy(&response.body));
Ok(())
}
async fn get_dnssec(args: DnssecGetCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(&client.get_dnssec(&args.zone_id).await?)
}
async fn edit_dnssec(args: DnssecEditCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(
&client
.edit_dnssec(
&args.zone_id,
&CloudflareDnssecEdit {
status: args.status,
dnssec_multi_signer: args.dnssec_multi_signer,
dnssec_presigned: args.dnssec_presigned,
dnssec_use_nsec3: args.dnssec_use_nsec3,
},
)
.await?,
)
}
async fn get_settings(args: crate::cli::commands::DnsSettingsGetCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
print_json(&client.get_dns_settings(&args.zone_id).await?)
}
async fn edit_settings(args: DnsSettingsEditCmd) -> Result<(), String> {
let client = cloudflare_client(args.provider, args.token, None)?;
let soa = match args.soa_json {
Some(raw) => Some(
serde_json::from_str::<CloudflareDnsSettingsSoa>(&raw)
.map_err(|error| format!("Invalid SOA JSON: {}", error))?,
),
None => None,
};
print_json(
&client
.edit_dns_settings(
&args.zone_id,
&CloudflareDnsSettings {
flatten_all_cnames: args.flatten_all_cnames,
foundation_dns: args.foundation_dns,
multi_provider: args.multi_provider,
nameservers: args.nameservers_type.map(|nameserver_type| {
CloudflareDnsSettingsNameservers {
r#type: Some(nameserver_type),
ns_set: args.nameservers_ns_set,
}
}),
ns_ttl: args.ns_ttl,
secondary_overrides: args.secondary_overrides,
soa,
zone_mode: args.zone_mode,
internal_dns: args.reference_zone_id.map(|reference_zone_id| {
CloudflareDnsSettingsInternalDns {
reference_zone_id: Some(reference_zone_id),
}
}),
},
)
.await?,
)
}
fn record_write_from_create(args: DnsRecordCreateCmd) -> Result<CloudflareDnsRecordWrite, String> {
Ok(CloudflareDnsRecordWrite {
record_type: Some(args.record_type),
name: Some(args.name),
content: Some(args.content),
ttl: args.ttl,
proxied: args.proxied,
priority: args.priority,
comment: args.comment,
tags: (!args.tags.is_empty()).then_some(args.tags),
data: parse_optional_json(args.data_json.as_deref())?,
settings: parse_optional_json(args.settings_json.as_deref())?,
})
}
fn parse_optional_json(raw: Option<&str>) -> Result<Option<Value>, String> {
raw.map(|value| {
serde_json::from_str::<Value>(value)
.map_err(|error| format!("Invalid JSON payload `{}`: {}", value, error))
})
.transpose()
}
fn cloudflare_client(
provider: DnsProviderKind,
token_override: Option<String>,
account_id_override: Option<String>,
) -> Result<CloudflareClient, String> {
if provider != DnsProviderKind::Cloudflare {
return Err(format!(
"Provider `{:?}` is planned but not implemented for `xbp dns` yet.",
provider
));
}
let token = token_override
.or_else(resolve_cloudflare_api_token)
.ok_or_else(|| {
"No Cloudflare API token found. Use `--token`, `CLOUDFLARE_API_TOKEN`, or `xbp config cloudflare set-key`.".to_string()
})?;
let account_id = account_id_override
.or_else(resolve_cloudflare_account_id)
.unwrap_or_default();
CloudflareClient::new(token, account_id)
}
fn print_json(value: &impl serde::Serialize) -> Result<(), String> {
println!(
"{}",
serde_json::to_string_pretty(value)
.map_err(|error| format!("Failed to encode JSON output: {}", error))?
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse_optional_json;
#[test]
fn parses_optional_json_strings() {
let value = parse_optional_json(Some(r#"{"ipv4_only":true}"#)).expect("json");
assert!(value.expect("value")["ipv4_only"].as_bool().unwrap());
}
}