Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion rust/src/dns/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::scopes::DOMAINS_DNS_UPDATE;

use super::conflicts::{WriteErrorContext, describe_write_error};
use super::records::{
RecordOptions, RecordWriteArgs, v3_records, validate_caa_fields, verify_with_list_action,
RecordOptions, RecordWriteArgs, v3_records, validate_caa_fields, validate_svcb_fields,
validate_tlsa_fields, verify_with_list_action,
};

// `dns add` creates one v3 record per `--data` value; it reports each outcome
Expand Down Expand Up @@ -126,6 +127,10 @@ pub(super) fn command() -> RuntimeCommandSpec {
let data = args.data;
validate_caa_fields(&record_type, &opts)
.map_err(crate::error::GddyError::validation)?;
validate_tlsa_fields(&record_type, &opts)
.map_err(crate::error::GddyError::validation)?;
validate_svcb_fields(&record_type, &opts)
.map_err(crate::error::GddyError::validation)?;
let records = v3_records(&name, &record_type, &data, &opts);

let debug = !ctx.middleware.debug.is_empty();
Expand Down
30 changes: 27 additions & 3 deletions rust/src/dns/conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::domain::{api_error, format_api_error};

use domains_client::types;

use super::records::fetch_records;
use super::records::{fetch_records, record_value};

/// A v3 validation-error body's `details[].issue` codes. Deliberately not
/// `domain::common`'s `ApiErrorBody` — that type is about rendering a friendly
Expand Down Expand Up @@ -106,14 +106,14 @@ pub(super) fn describe_duplicate_record(
"`{name}` already has a CNAME record (→ `{}`), which can't coexist with \
{desired_type} records — DNS only allows one or the other at a given \
name.{remediation}",
conflicts[0].data.as_deref().unwrap_or("(no data)"),
record_value(conflicts[0]).unwrap_or("(no data)"),
)
};
}

if at_name
.iter()
.any(|r| r.type_.as_str() == desired_type && r.data.as_deref() == Some(desired_data))
.any(|r| r.type_.as_str() == desired_type && record_value(r) == Some(desired_data))
{
return format!("a {desired_type} record with this exact value already exists at {name}.");
}
Expand Down Expand Up @@ -228,6 +228,16 @@ mod tests {
}
}

/// A TLSA record as `v3_record` actually builds one: its value lives in
/// `certificate_data`, not `data`.
fn tlsa_record(cert: &str) -> types::DnsRecord {
types::DnsRecord {
certificate_data: Some(cert.to_string()),
data: None,
..dns_record("TLSA", "")
}
}

#[test]
fn duplicate_record_issue_detects_the_v3_issue_code() {
// The exact body the reporting user got back from a `dns set` CNAME conflict.
Expand Down Expand Up @@ -329,4 +339,18 @@ mod tests {
"{msg}"
);
}

#[test]
fn describe_duplicate_record_recognizes_tlsa_exact_duplicates_via_certificate_data() {
// TLSA's value lives in `certificate_data`, not `data` — the exact-
// duplicate check must read through `record_value` to see it.
let cert = "d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971";
let exact = vec![tlsa_record(cert)];
let msg = describe_duplicate_record("TLSA", cert, "example.com", "www", &exact, true);
assert!(msg.contains("exact value already exists"), "{msg}");

let different = vec![tlsa_record("00")];
let msg = describe_duplicate_record("TLSA", cert, "example.com", "www", &different, true);
assert!(msg.contains("already has conflicting TLSA data"), "{msg}");
}
}
31 changes: 27 additions & 4 deletions rust/src/dns/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::scopes::DOMAINS_DNS_UPDATE;

use domains_client::types;

use super::records::{fetch_records, parse_write_type_arg, verify_with_list_action};
use super::records::{fetch_records, parse_write_type_arg, record_value, verify_with_list_action};

output_schema!(DnsDeleteResult {
"domain": "string";
Expand Down Expand Up @@ -85,7 +85,7 @@ fn dry_run_delete_preview(
} else {
"would fail (missing recordId)"
};
json!({"recordId": rec.record_id, "data": rec.data, "status": status})
json!({"recordId": rec.record_id, "data": record_value(rec), "status": status})
})
.collect();

Expand Down Expand Up @@ -117,7 +117,7 @@ struct DeleteArgs {
#[arg(value_name = "DOMAIN")]
domain: String,

/// Record type (A, AAAA, ALIAS, CAA, CNAME, MX, SRV, TXT).
/// Record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, SVCB, TLSA, TXT).
#[arg(long = "type", value_name = "TYPE", value_parser = parse_write_type_arg)]
record_type: String,

Expand Down Expand Up @@ -217,7 +217,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
}
},
};
outcomes.push((rec.data.as_deref().unwrap_or("(no data)").to_string(), err));
outcomes.push((record_value(rec).unwrap_or("(no data)").to_string(), err));
}

summarize_delete_outcomes(&domain, &record_type, &name, &outcomes)
Expand Down Expand Up @@ -296,6 +296,17 @@ mod tests {
}
}

/// A TLSA record as `v3_record` actually builds one: its value lives in
/// `certificate_data`, not `data`.
fn test_tlsa_record(record_id: &str, cert: &str) -> types::DnsRecord {
types::DnsRecord {
certificate_data: Some(cert.to_owned()),
data: None,
type_: types::DnsRecordType("TLSA".to_owned()),
..test_record(record_id, "")
}
}

#[test]
fn dry_run_delete_preview_lists_every_matched_record_without_deleting() {
let existing = vec![test_record("r1", "1.2.3.4"), test_record("r2", "5.6.7.8")];
Expand All @@ -309,6 +320,18 @@ mod tests {
assert_eq!(records[0]["status"], "would delete");
}

/// TLSA's value lives in `certificate_data`, not `data` — the preview
/// must read through `record_value` to show it rather than reporting
/// null.
#[test]
fn dry_run_delete_preview_shows_tlsa_certificate_data_as_the_value() {
let cert = "d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971";
let existing = vec![test_tlsa_record("r1", cert)];
let preview = dry_run_delete_preview("example.com", "TLSA", "www", &existing);
let records = preview["records"].as_array().expect("records array");
assert_eq!(records[0]["data"], cert);
}

/// A record without a recordId can't actually be deleted (the real
/// handler reports it as a failure) — the preview must not claim it as
/// a delete either.
Expand Down
4 changes: 2 additions & 2 deletions rust/src/dns/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ struct ListArgs {
#[arg(value_name = "DOMAIN")]
domain: String,

/// Only records of this type (A, AAAA, ALIAS, CAA, CNAME, MX, NS, SOA,
/// SRV, TXT).
/// Only records of this type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, NS,
/// SOA, SRV, SVCB, TLSA, TXT).
#[arg(long = "type", value_name = "TYPE", value_parser = parse_list_type_arg)]
record_type: Option<String>,

Expand Down
Loading