Repository: bottlerocket-os/bottlerocket
File: sources/api/netdog/src/cli/generate_hostname.rs (Rust)

Code before:
use super::{error, print_json, Result};
use crate::CURRENT_IP;
use argh::FromArgs;
use dns_lookup::lookup_addr;
use snafu::ResultExt;
use std::fs;
use std::net::IpAddr;
use std::str::FromStr;

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "generate-hostname")]
/// Generate hostname from DNS reverse lookup or use current IP
pub(crate) struct GenerateHostnameArgs {}

/// Attempt to resolve assigned IP address, if unsuccessful use the IP as the hostname.
///
/// The result is returned as JSON. (intended for use as a settings generator)
pub(crate) fn run() -> Result<()> {
    let ip_string = fs::read_to_string(CURRENT_IP)
        .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?;
    let ip = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?;
    let hostname = match lookup_addr(&ip) {
        Ok(hostname) => hostname,
        Err(e) => {
            eprintln!("Reverse DNS lookup failed: {}", e);
            ip_string
        }
    };

    // sundog expects JSON-serialized output
    print_json(hostname)
}


Review comment (bug):
This gives me pause. Elsewhere we have gone out of our way to segregate conditionally-compiled code from unconditionally-compiled code: https://github.com/bottlerocket-os/bottlerocket/blob/d20ea8efe911214ea635da81c2e8fed9c9703c77/sources/api/early-boot-config/src/provider.rs#L8..L21

I'm not sure that makes sense here, but I'm thinking about a case where we would want to query differently for a different provider  (vmware? or a different cloud, for example), and ending up with a string of sections of code with compiler guards.

Perhaps a compromise would be something like this:
(maybe it's too much trouble, but it's an idea)

```rust
    let hostname: Option<String> = Retry::spawn(retry_strategy(), || async { lookup_addr(&ip) })
        .await
        .or_else(|e| {
            eprintln!("Reverse DNS lookup failed: {}", e);
            ip_string
            Err(e)
        })
        .ok()
        .or_else(|| query_provider_hostname().await)
```

elsewhere:

```rust
async fn query_provider_hostname() -> Option<String> {
    #[cfg(variant_platform = "aws")]
    query_imds_hostname().await

    #[cfg(not(variant_platform = "aws"))]
    None
}

#[cfg(variant_platform = "aws")]
/// On AWS variants, we fallback to inspecting IMDS.
async fn query_imds_hostname() -> Option<String> {
    todo!()
}
```

Code after:
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "generate-hostname")]
/// Generate hostname from DNS reverse lookup or use current IP
pub(crate) struct GenerateHostnameArgs {}

/// Attempt to resolve assigned IP address, if unsuccessful use the IP as the hostname.
///
/// The result is returned as JSON. (intended for use as a settings generator)
pub(crate) async fn run() -> Result<()> {
    let ip_string = fs::read_to_string(CURRENT_IP)
        .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?;
    let ip = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?;

    // First, attempt to lookup the hostname via DNS
    let hostname: Option<String> = Retry::spawn(retry_strategy(), || async { lookup_addr(&ip) })
        .await
        .map_err(|e| {
            eprintln!("Reverse DNS lookup failed: {}", e);
            e
        })
        .ok();

    // If the DNS lookup fails, try any platform-specific mechanisms that exist.
    let hostname = if hostname.is_none() {
        // The interaction between async and `Result.or_else()` chaining makes this the most ergonomic way to write this...
        platform::query_platform_hostname()
            .await
            .map_err(|e| {
                eprintln!("Failed to find hostname from platform: {}", e);
                e
            })
            .ok()
            .flatten()
    } else {
        hostname
    }
    // If no hostname has been determined we return the IP address of the host.
    .unwrap_or(ip_string);

    // sundog expects JSON-serialized output
    print_json(hostname)
}

/// Returns an iterator of Durations to wait between retries of DNS queries.
fn retry_strategy() -> impl Iterator<Item = Duration> {
    FibonacciBackoff::from_millis(DNS_QUERY_FIBONACCI_RETRY_INTERVAL)
        .map(jitter)
        .take(DNS_QUERY_MAX_RETRIES)
}

mod platform {