datadog_agent_commons/
agent_version.rs

1//! Agent version detection.
2//!
3//! Agent Data Plane is designed to be a drop-in replacement for specific pieces of Datadog Agent functionality, and
4//! some of that behavior is version-dependent: a given Agent version may emit an output field, payload shape, or tag
5//! that an earlier version did not. To stay byte-for-byte compatible with the Agent it is bundled with, ADP needs to
6//! know which Agent version it is paired with.
7//!
8//! The Agent version is provided via the `DD_AGENT_VERSION` environment variable at *build time*, baked into the
9//! binary by `build.rs`. The converged Agent image build passes it as a Docker build argument, so every ADP binary
10//! carries a fixed, immutable version string that reflects the Agent it was compiled alongside.
11//!
12//! When `DD_AGENT_VERSION` is absent at build time (for example, in local development or standalone builds without a
13//! bundled Agent), the version is treated as unknown and all version gates default to the most recent behavior.
14//!
15//! # Design
16//!
17//! This is intentionally a lightweight, hand-rolled version parser rather than a full semantic-versioning
18//! implementation: the values we compare against are simple `MAJOR.MINOR.PATCH` release tags (optionally carrying a
19//! flavor or pre-release suffix such as `-full` or `-devel`), and we only ever need a "meets this minimum" comparison.
20
21include!(concat!(env!("OUT_DIR"), "/details.rs"));
22
23/// Raw Agent version string that ADP was built against, as detected at build time.
24///
25/// This is the verbatim `DD_AGENT_VERSION` value (for example, `"7.81.0-full"` or `"nightly"`), suitable for display in
26/// diagnostics. Returns `None` when `DD_AGENT_VERSION` was not set at build time.
27pub fn version_string() -> Option<&'static str> {
28    if DETECTED_AGENT_VERSION.is_empty() {
29        None
30    } else {
31        Some(DETECTED_AGENT_VERSION)
32    }
33}
34
35/// Version of the Datadog Agent that ADP is paired with, as detected at build time.
36///
37/// Returns `None` when `DD_AGENT_VERSION` was not set at build time.
38///
39/// This is a `const fn`: because the version is baked in at build time, callers can evaluate version gates in a
40/// `const` context so the compiler resolves the outcome statically.
41pub const fn version() -> Option<AgentVersion> {
42    if DETECTED_AGENT_VERSION.is_empty() {
43        return None;
44    }
45    Some(AgentVersion {
46        major: DETECTED_AGENT_VERSION_MAJOR,
47        minor: DETECTED_AGENT_VERSION_MINOR,
48        patch: DETECTED_AGENT_VERSION_PATCH,
49        is_dev: DETECTED_AGENT_IS_DEV,
50    })
51}
52
53/// Returns `true` if the Agent version is at least `major.minor.patch`.
54///
55/// When the Agent version is unknown (that is, `DD_AGENT_VERSION` was not set at build time), this returns `true`:
56/// absent an explicit older-version signal, ADP defaults to the most recent behavior.
57///
58/// This is a `const fn` so version gates can be evaluated at compile time (see [`version`]).
59pub const fn meets(major: u64, minor: u64, patch: u64) -> bool {
60    match version() {
61        Some(v) => v.meets(major, minor, patch),
62        None => true,
63    }
64}
65
66/// Version of the Datadog Agent that ADP is paired with.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub struct AgentVersion {
69    major: u64,
70    minor: u64,
71    patch: u64,
72    is_dev: bool,
73}
74
75impl AgentVersion {
76    /// Parses an Agent version from a version string.
77    ///
78    /// Accepts values such as `7.81.0`, `7.81.0-full`, `7.83.0-devel`, and bare development markers such as `nightly`.
79    /// Development/pre-release builds (identified by a `devel`, `dev`, `nightly`, or `master` marker) are considered
80    /// newer than every numbered release, since they track the bleeding edge of Agent behavior.
81    ///
82    /// Returns `None` if the string is empty or carries neither a numeric `MAJOR` component nor a development marker.
83    pub fn parse(raw: &str) -> Option<Self> {
84        let raw = raw.trim();
85        if raw.is_empty() {
86            return None;
87        }
88
89        let is_dev = ["devel", "dev", "nightly", "master"]
90            .iter()
91            .any(|marker| raw.contains(marker));
92
93        // Take the leading `MAJOR.MINOR.PATCH` portion, discarding any flavor/pre-release suffix (`-full`, `-devel`,
94        // `+git...`, etc.).
95        let version_part = raw.split(['-', '+']).next().unwrap_or("");
96        let mut components = version_part.split('.');
97        let major = components.next().and_then(|c| c.parse().ok());
98        let minor = components.next().and_then(|c| c.parse().ok()).unwrap_or(0);
99        let patch = components.next().and_then(|c| c.parse().ok()).unwrap_or(0);
100
101        match (major, is_dev) {
102            (Some(major), _) => Some(Self {
103                major,
104                minor,
105                patch,
106                is_dev,
107            }),
108            // A development marker with no numeric version (e.g. `nightly`) is still meaningful: it is newer than
109            // everything.
110            (None, true) => Some(Self {
111                major: 0,
112                minor: 0,
113                patch: 0,
114                is_dev: true,
115            }),
116            (None, false) => None,
117        }
118    }
119
120    /// Returns `true` if this version is at least `major.minor.patch`.
121    ///
122    /// Development/pre-release builds always satisfy the check, as they track behavior ahead of any numbered release.
123    pub const fn meets(&self, major: u64, minor: u64, patch: u64) -> bool {
124        if self.is_dev {
125            return true;
126        }
127        // Manual lexicographic comparison of (major, minor, patch); tuple `PartialOrd` is not usable in `const fn`.
128        if self.major != major {
129            return self.major > major;
130        }
131        if self.minor != minor {
132            return self.minor > minor;
133        }
134        self.patch >= patch
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn parses_plain_release() {
144        let version = AgentVersion::parse("7.81.0").expect("should parse");
145        assert_eq!(
146            version,
147            AgentVersion {
148                major: 7,
149                minor: 81,
150                patch: 0,
151                is_dev: false
152            }
153        );
154    }
155
156    #[test]
157    fn parses_release_with_flavor_suffix() {
158        let version = AgentVersion::parse("7.81.0-full").expect("should parse");
159        assert_eq!(
160            version,
161            AgentVersion {
162                major: 7,
163                minor: 81,
164                patch: 0,
165                is_dev: false
166            }
167        );
168    }
169
170    #[test]
171    fn parses_dev_build() {
172        let version = AgentVersion::parse("7.83.0-devel+git.20.5723c24").expect("should parse");
173        assert!(version.is_dev);
174        assert_eq!(version.major, 7);
175        assert_eq!(version.minor, 83);
176    }
177
178    #[test]
179    fn parses_bare_dev_marker() {
180        let version = AgentVersion::parse("nightly").expect("should parse");
181        assert!(version.is_dev);
182    }
183
184    #[test]
185    fn rejects_empty_and_garbage() {
186        assert_eq!(AgentVersion::parse(""), None);
187        assert_eq!(AgentVersion::parse("   "), None);
188        assert_eq!(AgentVersion::parse("not-a-version"), None);
189    }
190
191    #[test]
192    fn meets_compares_release_versions() {
193        let v = AgentVersion::parse("7.81.0").unwrap();
194        assert!(v.meets(7, 80, 0));
195        assert!(v.meets(7, 81, 0));
196        assert!(!v.meets(7, 82, 0));
197        assert!(!v.meets(8, 0, 0));
198    }
199
200    #[test]
201    fn dev_builds_meet_every_gate() {
202        let v = AgentVersion::parse("nightly").unwrap();
203        assert!(v.meets(7, 82, 0));
204        assert!(v.meets(99, 0, 0));
205
206        let v = AgentVersion::parse("7.83.0-devel").unwrap();
207        assert!(v.meets(7, 82, 0));
208        assert!(v.meets(99, 0, 0));
209    }
210}