Skip to main content

osdk_core/container/
mod.rs

1//! Native container-runtime contracts.
2//!
3//! This native-first layer deliberately contains no OCI store. It provides
4//! stable diagnostic, redaction, and injectable process boundaries plus one
5//! stale-checked atomic path for explicitly confirmed mirror configuration.
6
7pub mod apply;
8pub mod buildkit;
9pub mod cache;
10pub mod containerd;
11pub mod docker;
12pub mod mirror;
13pub mod operations;
14pub mod plan;
15pub mod redact;
16pub mod reference;
17pub mod registry;
18pub mod report;
19pub mod runtime;
20
21pub use apply::{
22    apply_mirror_plan, MirrorApplyError, MirrorApplyReport, MIRROR_APPLY_SCHEMA_VERSION,
23};
24pub use buildkit::{
25    BuildPlatform, BuilderDriver, BuilderNode, BuilderNodeStatus, BuildkitAdapter,
26    BuildkitDiagnosticDetails, BuildkitDiscovery, BuildkitNodeDiagnosticDetails,
27    BuildxBuilderSelector, BuildxBuilderSelectorError, SelectedBuilder,
28};
29pub use cache::{
30    BuildxCacheQuery, CacheQueryStatus, ContainerdCacheQuery, DockerCacheQuery, NativeCacheOwner,
31    NativeCacheRecord, NativeCacheRecordKind, NativeCacheStatus,
32    NATIVE_CACHE_STATUS_SCHEMA_VERSION,
33};
34pub use containerd::{
35    parse_effective_config, parse_hosts_toml, read_hosts_toml, ContainerdAdapter, ContainerdConfig,
36    ContainerdDiagnosticDetails, ContainerdDiscovery, ContainerdParseError, ContainerdVersions,
37    LegacyRegistryWarning, RegistryHost, RegistryHostCapability, RegistryHostTls, RegistryHosts,
38};
39pub use docker::{
40    DockerAdapter, DockerContext, DockerContextKind, DockerDaemonArchitecture, DockerDaemonOs,
41    DockerDiagnosticDetails, DockerDiscovery, DockerInfo, DockerParseError, DockerVersion,
42};
43pub use mirror::{
44    builtin_mirror_policy, plan_buildkit_mirrors, plan_containerd_mirrors, plan_docker_mirrors,
45    BuildkitMirrorPlanRequest, ContainerdMirrorPlanRequest, DockerMirrorPlanRequest,
46    MirrorPlanError, BUILTIN_DOCKER_HUB_MIRRORS, DOCKER_HUB_BENCHMARK_IMAGE,
47    DOCKER_HUB_BENCHMARK_PLATFORM,
48};
49pub use plan::{
50    ActivationRequirement, BuildkitTargetDriver, DockerTargetKind, EffectiveResolution,
51    Fingerprint, MirrorChange, MirrorPlan, MirrorPlanBundle, MirrorPlanDraft, MirrorPlanTarget,
52    NativeCandidateFingerprint, NativeConfigCandidate, NativeConfigFormat, NativeConfigSnapshot,
53    NativeInputFingerprint, NativeInputState, PlanApplicability, PlanError, PlanWarning,
54    PlannedCapability, PlannedMirrorEndpoint, RequiredPrivilege, ValidationStep,
55    MAX_NATIVE_CONFIG_BYTES, MIRROR_PLAN_SCHEMA_VERSION,
56};
57pub use reference::{
58    ImageReference, ImageSelector, ImageTag, OciDigest, OciPlatform, ReferenceError, RegistryName,
59    RepositoryName,
60};
61pub use registry::{
62    diagnose_registry, ApiCheck, ApiCheckStatus, BlobRangeCheck, BlobRangeStatus, ManifestCheck,
63    ManifestCheckStatus, ManifestKind, MirrorCheck, MirrorCheckStatus, RegistryDiagnosticOptions,
64    RegistryDiagnosticReport, RegistryDiagnosticStatus, RegistryEndpoint, RegistryLimits,
65    RegistryMethod, RegistryProtocolError, RegistryRequest, RegistryResponse, RegistryTransport,
66    RegistryTransportError, RegistryTransportFuture, ReqwestRegistryTransport,
67    DEFAULT_BLOB_SAMPLE_BYTES, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_MANIFEST_BYTES,
68    DEFAULT_MAX_REDIRECTS, DEFAULT_MAX_REQUESTS, DEFAULT_REQUEST_TIMEOUT, DEFAULT_TOTAL_TIMEOUT,
69    REGISTRY_DIAGNOSTIC_SCHEMA_VERSION,
70};
71
72pub use redact::{
73    CommandPurpose, HeaderName, NativeProgram, RedactedCommand, RedactedHeader, RedactedOrigin,
74    RedactedUrl, RedactedUrlError, RedactedValue, REDACTED,
75};
76pub use report::{
77    Capability, CapabilityStatus, DiagnosticDetails, DiagnosticEvidence, DiagnosticReport,
78    DiagnosticStatus, Endpoint, EndpointScope, EndpointTransport, Privilege, RuntimeKind,
79    DIAGNOSTIC_SCHEMA_VERSION,
80};
81pub use runtime::{ForegroundCommand, ProbeCommand, RuntimeAdapter};
82
83/// Convert one vendor-supplied version candidate into a semantic version.
84///
85/// Runtime-specific parsers remain responsible for locating a candidate in
86/// their output. This shared boundary accepts an optional `v`/`V` prefix,
87/// normalizes one- and two-component numeric versions, preserves valid SemVer
88/// prerelease/build metadata, and tolerates the `~vendor` suffix emitted by
89/// some distro packages. Extra numeric components are rejected.
90pub(crate) fn parse_vendor_version(candidate: &str) -> Option<semver::Version> {
91    let candidate = candidate.trim().trim_matches(|character: char| {
92        matches!(
93            character,
94            ',' | ';' | ':' | '(' | ')' | '[' | ']' | '{' | '}' | '"' | '\''
95        )
96    });
97    let candidate = candidate.strip_prefix(['v', 'V']).unwrap_or(candidate);
98    if let Ok(version) = semver::Version::parse(candidate) {
99        return Some(version);
100    }
101
102    // Debian-style versions commonly append `~ds1` or a similar packaging
103    // suffix which is not SemVer. Only discard that explicitly delimited
104    // suffix; never truncate an arbitrary or fourth numeric component.
105    if let Some((upstream, _vendor)) = candidate.split_once('~') {
106        if let Ok(version) = semver::Version::parse(upstream) {
107            return Some(version);
108        }
109    }
110
111    let components = candidate.split('.').collect::<Vec<_>>();
112    if !(1..=3).contains(&components.len())
113        || components.iter().any(|component| {
114            component.is_empty() || !component.bytes().all(|byte| byte.is_ascii_digit())
115        })
116    {
117        return None;
118    }
119    let major = components[0].parse().ok()?;
120    let minor = components.get(1).unwrap_or(&"0").parse().ok()?;
121    let patch = components.get(2).unwrap_or(&"0").parse().ok()?;
122    Some(semver::Version::new(major, minor, patch))
123}
124
125/// Classify a native endpoint without retaining any unredacted URL fields.
126/// Loopback TCP and HTTP(S) endpoints are local; SSH remains remote even when
127/// it targets loopback because it still crosses an administrative boundary.
128pub(crate) fn classify_endpoint(raw: &str) -> (EndpointTransport, EndpointScope) {
129    let scheme = raw
130        .split_once(':')
131        .map(|(scheme, _)| scheme)
132        .unwrap_or_default()
133        .to_ascii_lowercase();
134    match scheme.as_str() {
135        "unix" => (EndpointTransport::LocalSocket, EndpointScope::Local),
136        "npipe" => (EndpointTransport::NamedPipe, EndpointScope::Local),
137        "tcp" => (EndpointTransport::Tcp, network_endpoint_scope(raw)),
138        "http" => (EndpointTransport::Http, network_endpoint_scope(raw)),
139        "https" => (EndpointTransport::Https, network_endpoint_scope(raw)),
140        "ssh" => (EndpointTransport::Ssh, EndpointScope::Remote),
141        _ => (EndpointTransport::Unknown, EndpointScope::Unknown),
142    }
143}
144
145fn network_endpoint_scope(raw: &str) -> EndpointScope {
146    let local = reqwest::Url::parse(raw)
147        .ok()
148        .and_then(|url| url.host_str().map(str::to_owned))
149        .is_some_and(|host| {
150            let host = host
151                .trim_start_matches('[')
152                .trim_end_matches(']')
153                .trim_end_matches('.');
154            host.eq_ignore_ascii_case("localhost")
155                || host
156                    .parse::<std::net::IpAddr>()
157                    .is_ok_and(|address| address.is_loopback())
158        });
159    if local {
160        EndpointScope::Local
161    } else {
162        EndpointScope::Remote
163    }
164}
165
166#[cfg(test)]
167mod shared_tests {
168    use super::*;
169
170    #[test]
171    fn vendor_versions_have_one_consistent_strict_conversion() {
172        for (raw, expected) in [
173            ("1", Some(semver::Version::new(1, 0, 0))),
174            ("V1.2", Some(semver::Version::new(1, 2, 0))),
175            ("v1.2.3", Some(semver::Version::new(1, 2, 3))),
176            (
177                "(v1.2.3-rc.1+vendor)",
178                semver::Version::parse("1.2.3-rc.1+vendor").ok(),
179            ),
180            ("1.7.22~ds1-1", Some(semver::Version::new(1, 7, 22))),
181            ("1.2.3.4", None),
182            ("release-1.2.3", None),
183            ("", None),
184        ] {
185            assert_eq!(parse_vendor_version(raw), expected, "candidate: {raw}");
186        }
187    }
188
189    #[test]
190    fn loopback_network_endpoints_are_local_but_ssh_is_remote() {
191        for raw in [
192            "tcp://127.0.0.1:2375",
193            "http://LOCALHOST:2375",
194            "https://[::1]:2376",
195        ] {
196            assert_eq!(classify_endpoint(raw).1, EndpointScope::Local, "{raw}");
197        }
198        assert_eq!(
199            classify_endpoint("tcp://192.0.2.10:2375").1,
200            EndpointScope::Remote
201        );
202        assert_eq!(
203            classify_endpoint("ssh://localhost").1,
204            EndpointScope::Remote
205        );
206    }
207}