1use crate::source::{Source, SourceKind};
40use crate::{Resolver, sources};
41
42pub mod source_ids {
45 pub const ENV_OVERRIDE: &str = "env-override";
48 pub const FILE_OVERRIDE: &str = "file-override";
53 pub const CONTAINER: &str = "container";
55 pub const LXC: &str = "lxc";
57 pub const MACHINE_ID: &str = "machine-id";
59 pub const DBUS_MACHINE_ID: &str = "dbus-machine-id";
61 pub const DMI: &str = "dmi";
63 pub const LINUX_HOSTID: &str = "linux-hostid";
66 pub const IO_PLATFORM_UUID: &str = "io-platform-uuid";
68 pub const WINDOWS_MACHINE_GUID: &str = "windows-machine-guid";
70 pub const FREEBSD_HOSTID: &str = "freebsd-hostid";
72 pub const KENV_SMBIOS: &str = "kenv-smbios";
74 pub const BSD_KERN_HOSTID: &str = "bsd-kern-hostid";
76 pub const ILLUMOS_HOSTID: &str = "illumos-hostid";
78 pub const AWS_IMDS: &str = "aws-imds";
80 pub const GCP_METADATA: &str = "gcp-metadata";
82 pub const AZURE_IMDS: &str = "azure-imds";
84 pub const DIGITAL_OCEAN_METADATA: &str = "digital-ocean-metadata";
87 pub const HETZNER_METADATA: &str = "hetzner-metadata";
90 pub const OCI_METADATA: &str = "oci-metadata";
92 pub const OPENSTACK_METADATA: &str = "openstack-metadata";
95 pub const KUBERNETES_POD_UID: &str = "kubernetes-pod-uid";
97 pub const KUBERNETES_SERVICE_ACCOUNT: &str = "kubernetes-service-account";
99 pub const KUBERNETES_DOWNWARD_API: &str = "kubernetes-downward-api";
102}
103
104#[derive(Debug, thiserror::Error)]
106pub enum UnknownSourceError {
107 #[error("unknown source identifier: `{0}`")]
109 Unknown(String),
110 #[error(
114 "source `{0}` requires a caller-supplied path; construct it with its typed constructor and push it manually"
115 )]
116 RequiresPath(&'static str),
117 #[error("source `{0}` requires an HTTP transport; use resolver_from_ids_with_transport")]
120 RequiresTransport(&'static str),
121 #[error("source `{0}` is not available — the `{1}` feature is not enabled")]
124 FeatureDisabled(&'static str, &'static str),
125}
126
127pub fn resolver_from_ids<S, I>(ids: I) -> Result<Resolver, UnknownSourceError>
140where
141 S: AsRef<str>,
142 I: IntoIterator<Item = S>,
143{
144 let mut resolver = Resolver::new();
145 for id in ids {
146 let source = local_source_from_id(id.as_ref())?;
147 resolver = resolver.push_boxed(source);
148 }
149 Ok(resolver)
150}
151
152#[cfg(feature = "_transport")]
164#[allow(
165 clippy::needless_pass_by_value,
166 reason = "by-value transport matches `resolve_with_transport` and `Resolver::with_network_defaults`; the final clone drops the original"
167)]
168pub fn resolver_from_ids_with_transport<S, I, T>(
169 ids: I,
170 transport: T,
171) -> Result<Resolver, UnknownSourceError>
172where
173 S: AsRef<str>,
174 I: IntoIterator<Item = S>,
175 T: crate::transport::HttpTransport + Clone + 'static,
176{
177 let mut resolver = Resolver::new();
178 for id in ids {
179 let source = source_from_id_with_transport(id.as_ref(), transport.clone())?;
180 resolver = resolver.push_boxed(source);
181 }
182 Ok(resolver)
183}
184
185macro_rules! feature_ctor {
189 ($feature:literal, $id:literal, $ctor:expr) => {{
190 #[cfg(feature = $feature)]
191 {
192 Ok(Box::new($ctor))
193 }
194 #[cfg(not(feature = $feature))]
195 {
196 Err(UnknownSourceError::FeatureDisabled($id, $feature))
197 }
198 }};
199}
200
201fn local_source_from_id(id: &str) -> Result<Box<dyn Source>, UnknownSourceError> {
202 let kind = SourceKind::from_id(id).ok_or_else(|| UnknownSourceError::Unknown(id.to_owned()))?;
203 non_constructible_local(kind)
204 .or_else(|| feature_gated_local(kind))
205 .unwrap_or_else(|| Ok(plain_local(kind)))
206}
207
208fn non_constructible_local(
212 kind: SourceKind,
213) -> Option<Result<Box<dyn Source>, UnknownSourceError>> {
214 match kind {
215 SourceKind::EnvOverride => Some(Ok(Box::new(sources::EnvOverride::new("HOST_IDENTITY")))),
216 SourceKind::FileOverride => Some(Err(UnknownSourceError::RequiresPath("file-override"))),
217 SourceKind::KubernetesDownwardApi => Some(Err(UnknownSourceError::RequiresPath(
218 "kubernetes-downward-api",
219 ))),
220 SourceKind::AwsImds
221 | SourceKind::GcpMetadata
222 | SourceKind::AzureImds
223 | SourceKind::DigitalOceanMetadata
224 | SourceKind::HetznerMetadata
225 | SourceKind::OciMetadata
226 | SourceKind::OpenStackMetadata => {
227 Some(Err(UnknownSourceError::RequiresTransport(kind.as_str())))
228 }
229 _ => None,
230 }
231}
232
233fn feature_gated_local(kind: SourceKind) -> Option<Result<Box<dyn Source>, UnknownSourceError>> {
235 Some(match kind {
236 SourceKind::Container => {
237 feature_ctor!("container", "container", sources::ContainerId::default())
238 }
239 SourceKind::Lxc => feature_ctor!("container", "lxc", sources::LxcId::default()),
240 SourceKind::KubernetesPodUid => feature_ctor!(
241 "k8s",
242 "kubernetes-pod-uid",
243 sources::KubernetesPodUid::default()
244 ),
245 SourceKind::KubernetesServiceAccount => feature_ctor!(
246 "k8s",
247 "kubernetes-service-account",
248 sources::KubernetesServiceAccount::default()
249 ),
250 _ => return None,
251 })
252}
253
254fn plain_local(kind: SourceKind) -> Box<dyn Source> {
257 linux_family_source(kind)
258 .or_else(|| native_non_linux_source(kind))
259 .unwrap_or_else(|| unreachable!("plain_local reached with unhandled kind: {kind:?}"))
260}
261
262fn linux_family_source(kind: SourceKind) -> Option<Box<dyn Source>> {
263 Some(match kind {
264 SourceKind::MachineId => Box::new(sources::MachineIdFile::default()),
265 SourceKind::DbusMachineId => Box::new(sources::DbusMachineIdFile::default()),
266 SourceKind::Dmi => Box::new(sources::DmiProductUuid::default()),
267 SourceKind::LinuxHostId => Box::new(sources::LinuxHostIdFile::default()),
268 _ => return None,
269 })
270}
271
272fn native_non_linux_source(kind: SourceKind) -> Option<Box<dyn Source>> {
273 Some(match kind {
274 SourceKind::IoPlatformUuid => Box::new(sources::IoPlatformUuid::default()),
275 SourceKind::WindowsMachineGuid => Box::new(sources::WindowsMachineGuid::default()),
276 SourceKind::FreeBsdHostId => Box::new(sources::FreeBsdHostIdFile::default()),
277 SourceKind::KenvSmbios => Box::new(sources::KenvSmbios::default()),
278 SourceKind::BsdKernHostId => Box::new(sources::SysctlKernHostId::default()),
279 SourceKind::IllumosHostId => Box::new(sources::IllumosHostId::default()),
280 _ => return None,
281 })
282}
283
284#[cfg(feature = "_transport")]
285fn source_from_id_with_transport<T>(
286 id: &str,
287 transport: T,
288) -> Result<Box<dyn Source>, UnknownSourceError>
289where
290 T: crate::transport::HttpTransport + Clone + 'static,
291{
292 let kind = SourceKind::from_id(id).ok_or_else(|| UnknownSourceError::Unknown(id.to_owned()))?;
293 match kind {
294 SourceKind::AwsImds => feature_ctor!("aws", "aws-imds", sources::AwsImds::new(transport)),
295 SourceKind::GcpMetadata => {
296 feature_ctor!("gcp", "gcp-metadata", sources::GcpMetadata::new(transport))
297 }
298 SourceKind::AzureImds => {
299 feature_ctor!("azure", "azure-imds", sources::AzureImds::new(transport))
300 }
301 SourceKind::DigitalOceanMetadata => feature_ctor!(
302 "digitalocean",
303 "digital-ocean-metadata",
304 sources::DigitalOceanMetadata::new(transport)
305 ),
306 SourceKind::HetznerMetadata => feature_ctor!(
307 "hetzner",
308 "hetzner-metadata",
309 sources::HetznerMetadata::new(transport)
310 ),
311 SourceKind::OciMetadata => {
312 feature_ctor!("oci", "oci-metadata", sources::OciMetadata::new(transport))
313 }
314 SourceKind::OpenStackMetadata => feature_ctor!(
315 "openstack",
316 "openstack-metadata",
317 sources::OpenStackMetadata::new(transport)
318 ),
319 _ => {
320 drop(transport);
325 local_source_from_id(id)
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn source_kind_from_id_round_trips_every_builtin() {
336 for kind in [
337 SourceKind::EnvOverride,
338 SourceKind::FileOverride,
339 SourceKind::Container,
340 SourceKind::Lxc,
341 SourceKind::MachineId,
342 SourceKind::DbusMachineId,
343 SourceKind::Dmi,
344 SourceKind::LinuxHostId,
345 SourceKind::IoPlatformUuid,
346 SourceKind::WindowsMachineGuid,
347 SourceKind::FreeBsdHostId,
348 SourceKind::KenvSmbios,
349 SourceKind::BsdKernHostId,
350 SourceKind::IllumosHostId,
351 SourceKind::AwsImds,
352 SourceKind::GcpMetadata,
353 SourceKind::AzureImds,
354 SourceKind::DigitalOceanMetadata,
355 SourceKind::HetznerMetadata,
356 SourceKind::OciMetadata,
357 SourceKind::OpenStackMetadata,
358 SourceKind::KubernetesPodUid,
359 SourceKind::KubernetesServiceAccount,
360 SourceKind::KubernetesDownwardApi,
361 ] {
362 assert_eq!(SourceKind::from_id(kind.as_str()), Some(kind));
363 }
364 }
365
366 #[test]
374 fn local_source_from_id_handles_every_builtin_identifier() {
375 for id in [
376 source_ids::ENV_OVERRIDE,
377 source_ids::FILE_OVERRIDE,
378 source_ids::CONTAINER,
379 source_ids::LXC,
380 source_ids::MACHINE_ID,
381 source_ids::DBUS_MACHINE_ID,
382 source_ids::DMI,
383 source_ids::LINUX_HOSTID,
384 source_ids::IO_PLATFORM_UUID,
385 source_ids::WINDOWS_MACHINE_GUID,
386 source_ids::FREEBSD_HOSTID,
387 source_ids::KENV_SMBIOS,
388 source_ids::BSD_KERN_HOSTID,
389 source_ids::ILLUMOS_HOSTID,
390 source_ids::AWS_IMDS,
391 source_ids::GCP_METADATA,
392 source_ids::AZURE_IMDS,
393 source_ids::DIGITAL_OCEAN_METADATA,
394 source_ids::HETZNER_METADATA,
395 source_ids::OCI_METADATA,
396 source_ids::OPENSTACK_METADATA,
397 source_ids::KUBERNETES_POD_UID,
398 source_ids::KUBERNETES_SERVICE_ACCOUNT,
399 source_ids::KUBERNETES_DOWNWARD_API,
400 ] {
401 match local_source_from_id(id) {
402 Ok(_)
403 | Err(
404 UnknownSourceError::RequiresPath(_)
405 | UnknownSourceError::RequiresTransport(_)
406 | UnknownSourceError::FeatureDisabled(_, _),
407 ) => {}
408 Err(UnknownSourceError::Unknown(got)) => {
409 panic!("identifier `{id}` was reported as unknown (got `{got}`)");
410 }
411 }
412 }
413 }
414
415 #[test]
416 fn source_kind_from_id_rejects_unknown() {
417 assert_eq!(SourceKind::from_id("not-a-real-source"), None);
418 assert_eq!(SourceKind::from_id(""), None);
419 assert_eq!(SourceKind::from_id("my-custom-source"), None);
421 }
422
423 #[test]
424 fn resolver_from_ids_builds_chain_in_order() {
425 let resolver =
426 resolver_from_ids([source_ids::ENV_OVERRIDE, source_ids::MACHINE_ID]).unwrap();
427 assert_eq!(
428 resolver.source_kinds(),
429 vec![SourceKind::EnvOverride, SourceKind::MachineId]
430 );
431 }
432
433 #[test]
434 fn resolver_from_ids_rejects_unknown_identifier() {
435 match resolver_from_ids(["machine-id", "not-real"]).unwrap_err() {
436 UnknownSourceError::Unknown(s) => assert_eq!(s, "not-real"),
437 other => panic!("expected Unknown, got {other:?}"),
438 }
439 }
440
441 #[test]
442 fn resolver_from_ids_rejects_path_requiring_sources() {
443 match resolver_from_ids([source_ids::FILE_OVERRIDE]).unwrap_err() {
444 UnknownSourceError::RequiresPath(id) => assert_eq!(id, "file-override"),
445 other => panic!("expected RequiresPath, got {other:?}"),
446 }
447 #[cfg(feature = "k8s")]
448 match resolver_from_ids([source_ids::KUBERNETES_DOWNWARD_API]).unwrap_err() {
449 UnknownSourceError::RequiresPath(id) => {
450 assert_eq!(id, "kubernetes-downward-api");
451 }
452 other => panic!("expected RequiresPath, got {other:?}"),
453 }
454 }
455
456 #[cfg(feature = "aws")]
457 #[test]
458 fn resolver_from_ids_rejects_cloud_ids_without_transport() {
459 match resolver_from_ids([source_ids::AWS_IMDS]).unwrap_err() {
460 UnknownSourceError::RequiresTransport(id) => assert_eq!(id, "aws-imds"),
461 other => panic!("expected RequiresTransport, got {other:?}"),
462 }
463 }
464
465 #[cfg(feature = "aws")]
466 #[test]
467 fn resolver_from_ids_with_transport_accepts_cloud_ids() {
468 use crate::transport::HttpTransport;
469 use std::convert::Infallible;
470
471 #[derive(Clone)]
472 struct NoopTransport;
473 impl HttpTransport for NoopTransport {
474 type Error = Infallible;
475 fn send(
476 &self,
477 _req: http::Request<Vec<u8>>,
478 ) -> Result<http::Response<Vec<u8>>, Self::Error> {
479 Ok(http::Response::builder()
480 .status(404)
481 .body(Vec::new())
482 .unwrap())
483 }
484 }
485
486 let resolver = resolver_from_ids_with_transport(
487 [
488 source_ids::ENV_OVERRIDE,
489 source_ids::AWS_IMDS,
490 source_ids::MACHINE_ID,
491 ],
492 NoopTransport,
493 )
494 .unwrap();
495 assert_eq!(
496 resolver.source_kinds(),
497 vec![
498 SourceKind::EnvOverride,
499 SourceKind::AwsImds,
500 SourceKind::MachineId
501 ],
502 );
503 }
504
505 #[cfg(not(feature = "k8s"))]
506 #[test]
507 fn resolver_from_ids_reports_feature_disabled() {
508 match resolver_from_ids([source_ids::KUBERNETES_POD_UID]).unwrap_err() {
509 UnknownSourceError::FeatureDisabled(id, feat) => {
510 assert_eq!(id, "kubernetes-pod-uid");
511 assert_eq!(feat, "k8s");
512 }
513 other => panic!("expected FeatureDisabled, got {other:?}"),
514 }
515 }
516}