use std::{net::SocketAddr, path::PathBuf};
use clap::Args;
use onomancy_core::anchor::doc::DocAnchor;
use onomancy_dnssec::{
dns_name::DnsName,
txt::{generation_key::GenerationKey, record::TxtRecord},
validator::{Validator, WalkError},
};
use onomancy_hickory::provider::FetchChainError;
use onomancy_keyhive::{authority::KeyhiveAuthority, mint};
use onomancy_publish::{ceremony::migrate::Migrate as MigrateCeremony, signer::Signer};
use crate::{
now_ms, plan_io,
rotate::NotAGenerationKey,
seed::{self, SeedError},
};
#[derive(Debug, Args)]
pub(crate) struct Migrate {
#[arg(long)]
hostname: String,
#[arg(long)]
predecessor_key: Option<PathBuf>,
#[arg(long, conflicts_with = "predecessor_key")]
predecessor_seed: Option<String>,
#[arg(long)]
successor_key: Option<PathBuf>,
#[arg(long, conflicts_with = "successor_key")]
successor_seed: Option<String>,
#[arg(long)]
successor_generation_key: Option<PathBuf>,
#[arg(long, conflicts_with = "successor_generation_key")]
successor_generation_seed: Option<String>,
#[arg(long)]
resolver: Option<SocketAddr>,
#[arg(long, default_value = ".")]
out_dir: PathBuf,
}
impl Migrate {
pub(crate) fn run(&self) -> Result<(), MigrateError> {
let hostname = DnsName::parse_display(&self.hostname)?;
let predecessor_key = seed::load(
self.predecessor_seed.as_deref(),
self.predecessor_key.as_deref(),
)?;
let successor_key = seed::load(
self.successor_seed.as_deref(),
self.successor_key.as_deref(),
)?;
let successor_generation_key = seed::load(
self.successor_generation_seed.as_deref(),
self.successor_generation_key.as_deref(),
)?;
let successor_generation = GenerationKey::from(successor_generation_key.verifying_key());
let predecessor = DocAnchor::from(predecessor_key.verifying_key());
let retained = self.fetch_current(&hostname, &predecessor)?;
let plan = MigrateCeremony {
hostname,
predecessor,
successor: DocAnchor::from(successor_key.verifying_key()),
retained,
successor_generation,
lineage: vec![],
carriage: mint::generation_carriage(&successor_key, &successor_generation_key)?,
}
.plan(
now_ms(),
&Signer::new(predecessor_key),
&Signer::new(successor_key),
&KeyhiveAuthority,
)?;
crate::block_on(plan_io::execute(&plan, &self.out_dir))??;
Ok(())
}
fn fetch_current(
&self,
hostname: &DnsName,
predecessor: &DocAnchor,
) -> Result<TxtRecord, MigrateError> {
let provider = crate::provider(self.resolver);
let chain = crate::block_on(provider.fetch_chain(hostname))??;
let proof = Validator::iana().validate_detailed(hostname, &chain)?;
proof
.records
.iter()
.filter(|record| record.document() == predecessor)
.max_by_key(|record| record.serial())
.copied()
.ok_or(MigrateError::NoCurrentRecord)
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum MigrateError {
#[error(transparent)]
Ceremony(#[from] onomancy_publish::ceremony::CeremonyError),
#[error(transparent)]
Fetch(#[from] FetchChainError),
#[error(transparent)]
Generation(#[from] NotAGenerationKey),
#[error("hostname: {0}")]
Hostname(#[from] onomancy_dnssec::dns_name::ParseDnsNameError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Mint(#[from] onomancy_keyhive::mint::MintError),
#[error("no live record attests the predecessor document — nothing to migrate from")]
NoCurrentRecord,
#[error(transparent)]
Seed(#[from] SeedError),
#[error("live chain invalid: {0}")]
Walk(#[from] WalkError),
}