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}