Simplify DNS publication state and FlashDNS storage

This commit is contained in:
centra 2026-03-31 11:14:18 +09:00
parent aba1b73d5b
commit 1698009062
Signed by: centra
GPG key ID: 0C09689D20B25ACA
4 changed files with 45 additions and 140 deletions

View file

@ -896,14 +896,8 @@ pub struct PublishedLoadBalancerState {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PublishedDnsRecordState {
pub zone_id: String,
#[serde(default)]
pub record_id: String,
#[serde(default)]
pub record_ids: Vec<String>,
pub fqdn: String,
#[serde(default)]
pub value: String,
#[serde(default)]
pub values: Vec<String>,
}
@ -921,51 +915,6 @@ pub struct ServicePublicationState {
pub observed_at: Option<DateTime<Utc>>,
}
/// Publication record stored under photoncloud/clusters/{cluster_id}/publications/{service}.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct ServicePublicationRecord {
#[serde(default)]
pub service: String,
#[serde(default)]
pub org_id: Option<String>,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub lb_id: Option<String>,
#[serde(default)]
pub pool_id: Option<String>,
#[serde(default)]
pub listener_id: Option<String>,
#[serde(default)]
pub vip: Option<String>,
#[serde(default)]
pub listener_port: Option<u16>,
#[serde(default)]
pub listener_protocol: Option<String>,
#[serde(default)]
pub pool_protocol: Option<String>,
#[serde(default)]
pub backend_ids: HashMap<String, String>,
#[serde(default)]
pub backend_targets: HashMap<String, String>,
#[serde(default)]
pub dns_zone: Option<String>,
#[serde(default)]
pub dns_name: Option<String>,
#[serde(default)]
pub dns_mode: Option<DnsPublishMode>,
#[serde(default)]
pub dns_ttl: Option<u32>,
#[serde(default)]
pub zone_id: Option<String>,
#[serde(default)]
pub record_id: Option<String>,
#[serde(default)]
pub fqdn: Option<String>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
/// mTLS policy definition.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MtlsPolicySpec {
@ -1222,10 +1171,8 @@ mod tests {
}),
dns: Some(PublishedDnsRecordState {
zone_id: "zone-1".to_string(),
record_id: "record-1".to_string(),
record_ids: vec!["record-1".to_string()],
fqdn: "api.test.cluster.local".to_string(),
value: "10.0.0.50".to_string(),
values: vec!["10.0.0.50".to_string()],
}),
observed_at: None,

View file

@ -365,7 +365,6 @@ impl PublicationController {
let zone_name = normalize_zone_name(&spec.zone);
let record_name = record_name_for_service(spec, service);
let fqdn = format!("{}.{}", record_name, zone_name);
let primary_value = desired_values.first().cloned().unwrap_or_default();
if self.config.dry_run {
info!(
@ -376,10 +375,8 @@ impl PublicationController {
);
return Ok(existing.cloned().or(Some(PublishedDnsRecordState {
zone_id: String::new(),
record_id: String::new(),
record_ids: Vec::new(),
fqdn,
value: primary_value,
values: desired_values,
})));
}
@ -389,10 +386,11 @@ impl PublicationController {
let zone =
ensure_zone(&mut zone_client, auth_token, &zone_name, org_id, project_id).await?;
let replacing_existing =
existing.filter(|state| !dns_state_matches_target(state, &zone.id, &fqdn));
let records = ensure_records(
&mut record_client,
auth_token,
existing,
&zone.id,
&record_name,
spec.ttl,
@ -403,13 +401,14 @@ impl PublicationController {
.iter()
.map(|record| record.id.clone())
.collect::<Vec<_>>();
if let Some(previous) = replacing_existing {
self.cleanup_dns(auth_token, previous).await?;
}
Ok(Some(PublishedDnsRecordState {
zone_id: zone.id,
record_id: record_ids.first().cloned().unwrap_or_default(),
record_ids,
fqdn,
value: primary_value,
values: desired_values,
}))
}
@ -424,9 +423,6 @@ impl PublicationController {
};
let mut record_client = RecordServiceClient::connect(endpoint.clone()).await?;
let mut record_ids = dns_state.record_ids.clone();
if record_ids.is_empty() && !dns_state.record_id.is_empty() {
record_ids.push(dns_state.record_id.clone());
}
record_ids.sort();
record_ids.dedup();
@ -837,7 +833,6 @@ async fn ensure_zone(
async fn ensure_records(
client: &mut RecordServiceClient<tonic::transport::Channel>,
auth_token: &str,
existing: Option<&PublishedDnsRecordState>,
zone_id: &str,
name: &str,
ttl: u32,
@ -860,13 +855,7 @@ async fn ensure_records(
let mut matching = records
.iter()
.filter(|record| {
record.name == name
|| existing.map(|state| state.record_id.as_str()) == Some(record.id.as_str())
|| existing
.map(|state| state.record_ids.iter().any(|id| id == &record.id))
.unwrap_or(false)
})
.filter(|record| record.name == name)
.cloned()
.collect::<Vec<_>>();
matching.sort_by(|lhs, rhs| {
@ -1007,6 +996,10 @@ fn normalize_dns_values(values: impl IntoIterator<Item = String>) -> Vec<String>
values
}
fn dns_state_matches_target(state: &PublishedDnsRecordState, zone_id: &str, fqdn: &str) -> bool {
state.zone_id == zone_id && state.fqdn == fqdn
}
fn desired_dns_values(
spec: &DnsPublicationSpec,
healthy_instances: &[ServiceInstanceSpec],
@ -1324,6 +1317,32 @@ mod tests {
);
}
#[test]
fn test_dns_state_match_requires_same_zone_and_fqdn() {
let state = PublishedDnsRecordState {
zone_id: "zone-1".to_string(),
record_ids: vec!["record-1".to_string()],
fqdn: "api.native.cluster.test".to_string(),
values: vec!["10.0.0.11".to_string()],
};
assert!(dns_state_matches_target(
&state,
"zone-1",
"api.native.cluster.test"
));
assert!(!dns_state_matches_target(
&state,
"zone-2",
"api.native.cluster.test"
));
assert!(!dns_state_matches_target(
&state,
"zone-1",
"web.native.cluster.test"
));
}
#[test]
fn test_publishable_instance_requires_fresh_heartbeat() {
let now = Utc::now();

View file

@ -413,20 +413,6 @@ impl DnsMetadataStore {
format!("/flashdns/records/{}/", zone_id)
}
fn record_type_prefix(zone_id: &ZoneId, record_name: &str, record_type: RecordType) -> String {
format!(
"/flashdns/records/{}/{}/{}/",
zone_id, record_name, record_type
)
}
fn legacy_record_key(zone_id: &ZoneId, record_name: &str, record_type: RecordType) -> String {
format!(
"/flashdns/records/{}/{}/{}",
zone_id, record_name, record_type
)
}
fn record_id_key(record_id: &RecordId) -> String {
format!("/flashdns/record_ids/{}", record_id)
}
@ -566,37 +552,6 @@ impl DnsMetadataStore {
Ok(())
}
/// Load the first record by name and type, preserving compatibility with
/// older single-record keys.
pub async fn load_record(
&self,
zone_id: &ZoneId,
record_name: &str,
record_type: RecordType,
) -> Result<Option<Record>> {
let prefix = Self::record_type_prefix(zone_id, record_name, record_type);
let mut records = self
.get_prefix(&prefix)
.await?
.into_iter()
.filter_map(|(_, value)| serde_json::from_str::<Record>(&value).ok())
.collect::<Vec<_>>();
if records.is_empty() {
let legacy_key = Self::legacy_record_key(zone_id, record_name, record_type);
if let Some(value) = self.get(&legacy_key).await? {
let record: Record = serde_json::from_str(&value).map_err(|e| {
MetadataError::Serialization(format!("Failed to deserialize record: {}", e))
})?;
return Ok(Some(record));
}
return Ok(None);
}
records.sort_by(|lhs, rhs| lhs.id.to_string().cmp(&rhs.id.to_string()));
Ok(records.into_iter().next())
}
/// Load record by ID
pub async fn load_record_by_id(&self, record_id: &RecordId) -> Result<Option<Record>> {
let id_key = Self::record_id_key(record_id);
@ -628,12 +583,6 @@ impl DnsMetadataStore {
&record.id,
))
.await?;
self.delete_key(&Self::legacy_record_key(
&record.zone_id,
&record.name,
record.record_type,
))
.await?;
}
self.delete_key(&id_key).await?;
@ -867,25 +816,15 @@ mod tests {
// Save
store.save_record(&record).await.unwrap();
// Load
let loaded = store
.load_record(&zone.id, "www", RecordType::A)
.await
.unwrap()
.unwrap();
assert_eq!(loaded.id, record.id);
// List
let records = store.list_records(&zone.id).await.unwrap();
let records = store.list_records_by_name(&zone.id, "www").await.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].id, record.id);
// Delete
store.delete_record(&record).await.unwrap();
let deleted = store
.load_record(&zone.id, "www", RecordType::A)
.await
.unwrap();
assert!(deleted.is_none());
let deleted = store.list_records_by_name(&zone.id, "www").await.unwrap();
assert!(deleted.is_empty());
}
#[tokio::test]

View file

@ -5079,7 +5079,7 @@ validate_native_runtime_flow() {
local daemon_publication_value daemon_publication_fqdn
publication_value="$(native_publication_state "native-web")"
publication_fqdn="$(printf '%s' "${publication_value}" | jq -r '.dns.fqdn')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.values[0]')"
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
daemon_publication_value="$(native_publication_state "native-daemon")"
daemon_publication_fqdn="$(printf '%s' "${daemon_publication_value}" | jq -r '.dns.fqdn')"
@ -5143,7 +5143,7 @@ validate_native_runtime_flow() {
wait_for_http node01 "http://127.0.0.1:18191/" 240
publication_value="$(native_publication_state "native-web")"
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.values[0]')"
daemon_publication_value="$(native_publication_state "native-daemon")"
wait_for_native_lb_backends "${publication_pool_id}" "1" 180 10.100.0.22
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
@ -5191,7 +5191,7 @@ validate_native_runtime_flow() {
[[ "${restored_container_node}" == "node05" ]] || die "native-container unexpectedly moved after node04 returned to service"
publication_value="$(native_publication_state "native-web")"
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.values[0]')"
daemon_publication_value="$(native_publication_state "native-daemon")"
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
@ -5228,7 +5228,7 @@ validate_native_runtime_flow() {
wait_for_native_instance_node "native-daemon" "node04" 240 >/dev/null
publication_value="$(native_publication_state "native-web")"
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.values[0]')"
daemon_publication_value="$(native_publication_state "native-daemon")"
wait_for_native_lb_backends "${publication_pool_id}" "1" 240 10.100.0.21
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
@ -5284,7 +5284,7 @@ validate_native_runtime_flow() {
[[ "${recovered_container_node}" == "node04" ]] || die "native-container unexpectedly churned after node05 recovered"
publication_value="$(native_publication_state "native-web")"
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.values[0]')"
daemon_publication_value="$(native_publication_state "native-daemon")"
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180