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 LINUX_HOSTID: &str = "linux-hostid";
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 OPENSTACK_METADATA: &str = "openstack-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()))?;
non_constructible_local(kind)
.or_else(|| feature_gated_local(kind))
.unwrap_or_else(|| Ok(plain_local(kind)))
}
fn non_constructible_local(
kind: SourceKind,
) -> Option<Result<Box<dyn Source>, UnknownSourceError>> {
match kind {
SourceKind::EnvOverride => Some(Ok(Box::new(sources::EnvOverride::new("HOST_IDENTITY")))),
SourceKind::FileOverride => Some(Err(UnknownSourceError::RequiresPath("file-override"))),
SourceKind::KubernetesDownwardApi => Some(Err(UnknownSourceError::RequiresPath(
"kubernetes-downward-api",
))),
SourceKind::AwsImds
| SourceKind::GcpMetadata
| SourceKind::AzureImds
| SourceKind::DigitalOceanMetadata
| SourceKind::HetznerMetadata
| SourceKind::OciMetadata
| SourceKind::OpenStackMetadata => {
Some(Err(UnknownSourceError::RequiresTransport(kind.as_str())))
}
_ => None,
}
}
fn feature_gated_local(kind: SourceKind) -> Option<Result<Box<dyn Source>, UnknownSourceError>> {
Some(match kind {
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()
),
_ => return None,
})
}
fn plain_local(kind: SourceKind) -> Box<dyn Source> {
linux_family_source(kind)
.or_else(|| native_non_linux_source(kind))
.unwrap_or_else(|| unreachable!("plain_local reached with unhandled kind: {kind:?}"))
}
fn linux_family_source(kind: SourceKind) -> Option<Box<dyn Source>> {
Some(match kind {
SourceKind::MachineId => Box::new(sources::MachineIdFile::default()),
SourceKind::DbusMachineId => Box::new(sources::DbusMachineIdFile::default()),
SourceKind::Dmi => Box::new(sources::DmiProductUuid::default()),
SourceKind::LinuxHostId => Box::new(sources::LinuxHostIdFile::default()),
_ => return None,
})
}
fn native_non_linux_source(kind: SourceKind) -> Option<Box<dyn Source>> {
Some(match kind {
SourceKind::IoPlatformUuid => Box::new(sources::IoPlatformUuid::default()),
SourceKind::WindowsMachineGuid => Box::new(sources::WindowsMachineGuid::default()),
SourceKind::FreeBsdHostId => Box::new(sources::FreeBsdHostIdFile::default()),
SourceKind::KenvSmbios => Box::new(sources::KenvSmbios::default()),
SourceKind::BsdKernHostId => Box::new(sources::SysctlKernHostId::default()),
SourceKind::IllumosHostId => Box::new(sources::IllumosHostId::default()),
_ => return None,
})
}
#[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))
}
SourceKind::OpenStackMetadata => feature_ctor!(
"openstack",
"openstack-metadata",
sources::OpenStackMetadata::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::LinuxHostId,
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::OpenStackMetadata,
SourceKind::KubernetesPodUid,
SourceKind::KubernetesServiceAccount,
SourceKind::KubernetesDownwardApi,
] {
assert_eq!(SourceKind::from_id(kind.as_str()), Some(kind));
}
}
#[test]
fn local_source_from_id_handles_every_builtin_identifier() {
for id in [
source_ids::ENV_OVERRIDE,
source_ids::FILE_OVERRIDE,
source_ids::CONTAINER,
source_ids::LXC,
source_ids::MACHINE_ID,
source_ids::DBUS_MACHINE_ID,
source_ids::DMI,
source_ids::LINUX_HOSTID,
source_ids::IO_PLATFORM_UUID,
source_ids::WINDOWS_MACHINE_GUID,
source_ids::FREEBSD_HOSTID,
source_ids::KENV_SMBIOS,
source_ids::BSD_KERN_HOSTID,
source_ids::ILLUMOS_HOSTID,
source_ids::AWS_IMDS,
source_ids::GCP_METADATA,
source_ids::AZURE_IMDS,
source_ids::DIGITAL_OCEAN_METADATA,
source_ids::HETZNER_METADATA,
source_ids::OCI_METADATA,
source_ids::OPENSTACK_METADATA,
source_ids::KUBERNETES_POD_UID,
source_ids::KUBERNETES_SERVICE_ACCOUNT,
source_ids::KUBERNETES_DOWNWARD_API,
] {
match local_source_from_id(id) {
Ok(_)
| Err(
UnknownSourceError::RequiresPath(_)
| UnknownSourceError::RequiresTransport(_)
| UnknownSourceError::FeatureDisabled(_, _),
) => {}
Err(UnknownSourceError::Unknown(got)) => {
panic!("identifier `{id}` was reported as unknown (got `{got}`)");
}
}
}
}
#[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:?}"),
}
}
}