use crate::source::{Source, SourceKind};
use crate::{Resolver, sources};
pub mod source_ids {
pub const ENV_OVERRIDE: &str = "env-override";
pub const FILE_OVERRIDE: &str = "file-override";
pub const CONTAINER: &str = "container";
pub const LXC: &str = "lxc";
pub const MACHINE_ID: &str = "machine-id";
pub const DBUS_MACHINE_ID: &str = "dbus-machine-id";
pub const DMI: &str = "dmi";
pub const IO_PLATFORM_UUID: &str = "io-platform-uuid";
pub const WINDOWS_MACHINE_GUID: &str = "windows-machine-guid";
pub const FREEBSD_HOSTID: &str = "freebsd-hostid";
pub const KENV_SMBIOS: &str = "kenv-smbios";
pub const BSD_KERN_HOSTID: &str = "bsd-kern-hostid";
pub const ILLUMOS_HOSTID: &str = "illumos-hostid";
pub const AWS_IMDS: &str = "aws-imds";
pub const GCP_METADATA: &str = "gcp-metadata";
pub const AZURE_IMDS: &str = "azure-imds";
pub const DIGITAL_OCEAN_METADATA: &str = "digital-ocean-metadata";
pub const HETZNER_METADATA: &str = "hetzner-metadata";
pub const OCI_METADATA: &str = "oci-metadata";
pub const KUBERNETES_POD_UID: &str = "kubernetes-pod-uid";
pub const KUBERNETES_SERVICE_ACCOUNT: &str = "kubernetes-service-account";
pub const KUBERNETES_DOWNWARD_API: &str = "kubernetes-downward-api";
}
#[derive(Debug, thiserror::Error)]
pub enum UnknownSourceError {
#[error("unknown source identifier: `{0}`")]
Unknown(String),
#[error(
"source `{0}` requires a caller-supplied path; construct it with its typed constructor and push it manually"
)]
RequiresPath(&'static str),
#[error("source `{0}` requires an HTTP transport; use resolver_from_ids_with_transport")]
RequiresTransport(&'static str),
#[error("source `{0}` is not available — the `{1}` feature is not enabled")]
FeatureDisabled(&'static str, &'static str),
}
pub fn resolver_from_ids<S, I>(ids: I) -> Result<Resolver, UnknownSourceError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let mut resolver = Resolver::new();
for id in ids {
let source = local_source_from_id(id.as_ref())?;
resolver = resolver.push_boxed(source);
}
Ok(resolver)
}
#[cfg(feature = "_transport")]
#[allow(
clippy::needless_pass_by_value,
reason = "by-value transport matches `resolve_with_transport` and `Resolver::with_network_defaults`; the final clone drops the original"
)]
pub fn resolver_from_ids_with_transport<S, I, T>(
ids: I,
transport: T,
) -> Result<Resolver, UnknownSourceError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
T: crate::transport::HttpTransport + Clone + 'static,
{
let mut resolver = Resolver::new();
for id in ids {
let source = source_from_id_with_transport(id.as_ref(), transport.clone())?;
resolver = resolver.push_boxed(source);
}
Ok(resolver)
}
macro_rules! feature_ctor {
($feature:literal, $id:literal, $ctor:expr) => {{
#[cfg(feature = $feature)]
{
Ok(Box::new($ctor))
}
#[cfg(not(feature = $feature))]
{
Err(UnknownSourceError::FeatureDisabled($id, $feature))
}
}};
}
fn local_source_from_id(id: &str) -> Result<Box<dyn Source>, UnknownSourceError> {
let kind = SourceKind::from_id(id).ok_or_else(|| UnknownSourceError::Unknown(id.to_owned()))?;
match kind {
SourceKind::EnvOverride => Ok(Box::new(sources::EnvOverride::new("HOST_IDENTITY"))),
SourceKind::FileOverride => Err(UnknownSourceError::RequiresPath("file-override")),
SourceKind::KubernetesDownwardApi => {
Err(UnknownSourceError::RequiresPath("kubernetes-downward-api"))
}
SourceKind::AwsImds
| SourceKind::GcpMetadata
| SourceKind::AzureImds
| SourceKind::DigitalOceanMetadata
| SourceKind::HetznerMetadata
| SourceKind::OciMetadata => Err(UnknownSourceError::RequiresTransport(kind.as_str())),
SourceKind::Container => {
feature_ctor!("container", "container", sources::ContainerId::default())
}
SourceKind::Lxc => {
feature_ctor!("container", "lxc", sources::LxcId::default())
}
SourceKind::KubernetesPodUid => {
feature_ctor!(
"k8s",
"kubernetes-pod-uid",
sources::KubernetesPodUid::default()
)
}
SourceKind::KubernetesServiceAccount => feature_ctor!(
"k8s",
"kubernetes-service-account",
sources::KubernetesServiceAccount::default()
),
SourceKind::MachineId => Ok(Box::new(sources::MachineIdFile::default())),
SourceKind::DbusMachineId => Ok(Box::new(sources::DbusMachineIdFile::default())),
SourceKind::Dmi => Ok(Box::new(sources::DmiProductUuid::default())),
SourceKind::IoPlatformUuid => Ok(Box::new(sources::IoPlatformUuid::default())),
SourceKind::WindowsMachineGuid => Ok(Box::new(sources::WindowsMachineGuid::default())),
SourceKind::FreeBsdHostId => Ok(Box::new(sources::FreeBsdHostIdFile::default())),
SourceKind::KenvSmbios => Ok(Box::new(sources::KenvSmbios::default())),
SourceKind::BsdKernHostId => Ok(Box::new(sources::SysctlKernHostId::default())),
SourceKind::IllumosHostId => Ok(Box::new(sources::IllumosHostId::default())),
SourceKind::Custom(_) => unreachable!("from_id never returns Custom"),
}
}
#[cfg(feature = "_transport")]
fn source_from_id_with_transport<T>(
id: &str,
transport: T,
) -> Result<Box<dyn Source>, UnknownSourceError>
where
T: crate::transport::HttpTransport + Clone + 'static,
{
let kind = SourceKind::from_id(id).ok_or_else(|| UnknownSourceError::Unknown(id.to_owned()))?;
match kind {
SourceKind::AwsImds => feature_ctor!("aws", "aws-imds", sources::AwsImds::new(transport)),
SourceKind::GcpMetadata => {
feature_ctor!("gcp", "gcp-metadata", sources::GcpMetadata::new(transport))
}
SourceKind::AzureImds => {
feature_ctor!("azure", "azure-imds", sources::AzureImds::new(transport))
}
SourceKind::DigitalOceanMetadata => feature_ctor!(
"digitalocean",
"digital-ocean-metadata",
sources::DigitalOceanMetadata::new(transport)
),
SourceKind::HetznerMetadata => feature_ctor!(
"hetzner",
"hetzner-metadata",
sources::HetznerMetadata::new(transport)
),
SourceKind::OciMetadata => {
feature_ctor!("oci", "oci-metadata", sources::OciMetadata::new(transport))
}
_ => {
drop(transport);
local_source_from_id(id)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_kind_from_id_round_trips_every_builtin() {
for kind in [
SourceKind::EnvOverride,
SourceKind::FileOverride,
SourceKind::Container,
SourceKind::Lxc,
SourceKind::MachineId,
SourceKind::DbusMachineId,
SourceKind::Dmi,
SourceKind::IoPlatformUuid,
SourceKind::WindowsMachineGuid,
SourceKind::FreeBsdHostId,
SourceKind::KenvSmbios,
SourceKind::BsdKernHostId,
SourceKind::IllumosHostId,
SourceKind::AwsImds,
SourceKind::GcpMetadata,
SourceKind::AzureImds,
SourceKind::DigitalOceanMetadata,
SourceKind::HetznerMetadata,
SourceKind::OciMetadata,
SourceKind::KubernetesPodUid,
SourceKind::KubernetesServiceAccount,
SourceKind::KubernetesDownwardApi,
] {
assert_eq!(SourceKind::from_id(kind.as_str()), Some(kind));
}
}
#[test]
fn source_kind_from_id_rejects_unknown() {
assert_eq!(SourceKind::from_id("not-a-real-source"), None);
assert_eq!(SourceKind::from_id(""), None);
assert_eq!(SourceKind::from_id("my-custom-source"), None);
}
#[test]
fn resolver_from_ids_builds_chain_in_order() {
let resolver =
resolver_from_ids([source_ids::ENV_OVERRIDE, source_ids::MACHINE_ID]).unwrap();
assert_eq!(
resolver.source_kinds(),
vec![SourceKind::EnvOverride, SourceKind::MachineId]
);
}
#[test]
fn resolver_from_ids_rejects_unknown_identifier() {
match resolver_from_ids(["machine-id", "not-real"]).unwrap_err() {
UnknownSourceError::Unknown(s) => assert_eq!(s, "not-real"),
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn resolver_from_ids_rejects_path_requiring_sources() {
match resolver_from_ids([source_ids::FILE_OVERRIDE]).unwrap_err() {
UnknownSourceError::RequiresPath(id) => assert_eq!(id, "file-override"),
other => panic!("expected RequiresPath, got {other:?}"),
}
#[cfg(feature = "k8s")]
match resolver_from_ids([source_ids::KUBERNETES_DOWNWARD_API]).unwrap_err() {
UnknownSourceError::RequiresPath(id) => {
assert_eq!(id, "kubernetes-downward-api");
}
other => panic!("expected RequiresPath, got {other:?}"),
}
}
#[cfg(feature = "aws")]
#[test]
fn resolver_from_ids_rejects_cloud_ids_without_transport() {
match resolver_from_ids([source_ids::AWS_IMDS]).unwrap_err() {
UnknownSourceError::RequiresTransport(id) => assert_eq!(id, "aws-imds"),
other => panic!("expected RequiresTransport, got {other:?}"),
}
}
#[cfg(feature = "aws")]
#[test]
fn resolver_from_ids_with_transport_accepts_cloud_ids() {
use crate::transport::HttpTransport;
use std::convert::Infallible;
#[derive(Clone)]
struct NoopTransport;
impl HttpTransport for NoopTransport {
type Error = Infallible;
fn send(
&self,
_req: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>, Self::Error> {
Ok(http::Response::builder()
.status(404)
.body(Vec::new())
.unwrap())
}
}
let resolver = resolver_from_ids_with_transport(
[
source_ids::ENV_OVERRIDE,
source_ids::AWS_IMDS,
source_ids::MACHINE_ID,
],
NoopTransport,
)
.unwrap();
assert_eq!(
resolver.source_kinds(),
vec![
SourceKind::EnvOverride,
SourceKind::AwsImds,
SourceKind::MachineId
],
);
}
#[cfg(not(feature = "k8s"))]
#[test]
fn resolver_from_ids_reports_feature_disabled() {
match resolver_from_ids([source_ids::KUBERNETES_POD_UID]).unwrap_err() {
UnknownSourceError::FeatureDisabled(id, feat) => {
assert_eq!(id, "kubernetes-pod-uid");
assert_eq!(feat, "k8s");
}
other => panic!("expected FeatureDisabled, got {other:?}"),
}
}
}