Skip to content

Commit 2fe82f3

Browse files
feat: Multipackage agent version (#2671)
* feat: Multipackage agent version
1 parent eae4619 commit 2fe82f3

6 files changed

Lines changed: 239 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Remember that the keywords that you can use are the following:
1919
- RPM packages now restore `local_config.yaml` from the `.rpmsave` backup left by a prior uninstall.
2020

2121
### enhancement
22+
- On-host agent-type parsing now validates `reported_version_package`: it must reference a declared package, and is required when more than one package is defined. Invalid configurations are rejected at parse time with a descriptive error. This `reported_version_package` will be used to know which version to report as `agent.version`.
2223
- Add support for `copy_from_file` for on-host "in-agent" filesystem.
2324
- Add support for `shared_filesystem` in on-host agent types.
2425
- On-host self-update: skip sub-agent reconciliation when a self-update is in progress. When a single remote config both bumps the Agent Control version and changes a sub-agent's `agent_type`, the changed sub-agent is no longer recreated moments before the process restarts; the new config is stored and re-applied cleanly by the restarted process, avoiding a redundant sub-agent restart and telemetry gap.

agent-control/src/agent_type/README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,36 @@ deployment:
255255
version: ${nr-var:package_version}
256256
```
257257

258-
The resolved value of that `version` field is published as `agent.version`. Which package reports
259-
the version when an agent type defines more than one is not yet finalized. See
260-
[On Host Packages](#on-host-packages) for the full package configuration.
258+
The resolved value of that `version` field is published as `agent.version`.
259+
260+
When an agent type defines **more than one package**, the package to report is selected explicitly
261+
with the top-level `deployment.reported_version_package` field, which must name one of the declared
262+
packages:
263+
264+
```yaml
265+
deployment:
266+
reported_version_package: newrelic-infra # required when more than one package is declared
267+
packages:
268+
newrelic-infra:
269+
download:
270+
oci:
271+
repository: newrelic-infra
272+
version: ${nr-var:package_version}
273+
nri-flex:
274+
download:
275+
oci:
276+
repository: nri-flex
277+
version: ${nr-var:flex_version}
278+
```
279+
280+
Rules:
281+
282+
* With **one** package, `reported_version_package` is optional and defaults to that sole package.
283+
* With **more than one** package, `reported_version_package` is **required** and must reference a declared
284+
package id; otherwise the agent type fails validation at parse time.
285+
* With **no** packages, no `agent.version` is reported.
286+
287+
See [On Host Packages](#on-host-packages) for the full package configuration.
261288

262289
### Kubernetes Deployment
263290

agent-control/src/agent_type/runtime_config/on_host.rs

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,88 @@ pub mod rendered;
1919
/// The definition for an on-host supervisor.
2020
///
2121
/// It contains the instructions of what are the agent binaries, command-line arguments, the environment variables passed to it and the restart policy of the supervisor.
22-
#[derive(Debug, Deserialize, Default, Clone, PartialEq)]
22+
#[derive(Debug, Default, Clone, PartialEq)]
2323
pub struct OnHost {
24-
#[serde(deserialize_with = "deserialize_executables", default)]
2524
executables: Vec<Executable>,
26-
#[serde(default)]
2725
enable_file_logging: TemplateableValue<bool>,
28-
/// Enables and define health checks configuration.
29-
#[serde(default)]
26+
/// Enables and defines health checks configuration.
3027
health: Option<OnHostHealthConfig>,
31-
#[serde(default)]
3228
filesystem: FileSystem,
33-
#[serde(default)]
34-
shared_filesystem: SharedFileSystem,
35-
#[serde(default)]
3629
packages: Packages,
30+
shared_filesystem: SharedFileSystem,
31+
/// Package whose OCI version is reported as the `agent.version` identifying attribute.
32+
reported_version_package: Option<PackageID>,
3733
}
3834

3935
type Packages = HashMap<PackageID, Package>;
4036

37+
impl<'de> Deserialize<'de> for OnHost {
38+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39+
where
40+
D: Deserializer<'de>,
41+
{
42+
#[derive(Deserialize)]
43+
struct OnHostRaw {
44+
#[serde(deserialize_with = "deserialize_executables", default)]
45+
executables: Vec<Executable>,
46+
#[serde(default)]
47+
enable_file_logging: TemplateableValue<bool>,
48+
#[serde(default)]
49+
health: Option<OnHostHealthConfig>,
50+
#[serde(default)]
51+
filesystem: FileSystem,
52+
#[serde(default)]
53+
shared_filesystem: SharedFileSystem,
54+
#[serde(default)]
55+
packages: Packages,
56+
#[serde(default)]
57+
reported_version_package: Option<PackageID>,
58+
}
59+
60+
let raw = OnHostRaw::deserialize(deserializer)?;
61+
let reported_version_package =
62+
resolve_reported_version_package(&raw.packages, raw.reported_version_package)?;
63+
Ok(OnHost {
64+
executables: raw.executables,
65+
enable_file_logging: raw.enable_file_logging,
66+
health: raw.health,
67+
filesystem: raw.filesystem,
68+
shared_filesystem: raw.shared_filesystem,
69+
packages: raw.packages,
70+
reported_version_package,
71+
})
72+
}
73+
}
74+
75+
/// Resolves which package's OCI version is reported as `agent.version`:
76+
/// - an explicit `reported_version_package` must reference a declared package;
77+
/// - with no packages declared, there is nothing to report (`None`);
78+
/// - with exactly one package, it defaults to that package;
79+
/// - with more than one package, `reported_version_package` is required.
80+
fn resolve_reported_version_package<E: serde::de::Error>(
81+
packages: &Packages,
82+
declared: Option<PackageID>,
83+
) -> Result<Option<PackageID>, E> {
84+
if let Some(id) = declared {
85+
if packages.contains_key(&id) {
86+
return Ok(Some(id));
87+
}
88+
return Err(E::custom(format!(
89+
"`reported_version_package` references unknown package `{id}`; declared packages: [{}]",
90+
packages.keys().cloned().collect::<Vec<_>>().join(", ")
91+
)));
92+
}
93+
94+
match packages.len() {
95+
0 => Ok(None),
96+
1 => Ok(packages.keys().next().cloned()),
97+
_ => Err(E::custom(format!(
98+
"`reported_version_package` is required when more than one package is defined; declared packages: [{}]",
99+
packages.keys().cloned().collect::<Vec<_>>().join(", ")
100+
))),
101+
}
102+
}
103+
41104
impl OnHost {
42105
/// The files and directories this Agent Type declares in the shared filesystem.
43106
/// The paths are static because they are not [Templateable], therefore they are
@@ -96,6 +159,7 @@ impl Templateable for OnHost {
96159
filesystem: self.filesystem.template_with(&extended_vars)?,
97160
shared_filesystem: self.shared_filesystem.template_with(&extended_vars)?,
98161
packages: rendered_packages,
162+
reported_version_package: self.reported_version_package,
99163
})
100164
}
101165
}
@@ -233,6 +297,106 @@ packages:
233297
);
234298
}
235299

300+
#[test]
301+
fn reported_version_package_defaults_to_sole_package() {
302+
let yaml = r#"
303+
packages:
304+
infra:
305+
download:
306+
oci:
307+
repository: newrelic-infra
308+
version: "1.2.3"
309+
"#;
310+
let on_host: OnHost = serde_saphyr::from_str(yaml).unwrap();
311+
assert_eq!(on_host.reported_version_package, Some("infra".to_string()));
312+
}
313+
314+
#[test]
315+
fn reported_version_package_explicit_selection_with_multiple_packages() {
316+
let yaml = r#"
317+
reported_version_package: infra
318+
packages:
319+
infra:
320+
download:
321+
oci:
322+
repository: newrelic-infra
323+
version: "1.2.3"
324+
flex:
325+
download:
326+
oci:
327+
repository: nri-flex
328+
version: "4.5.6"
329+
"#;
330+
let on_host: OnHost = serde_saphyr::from_str(yaml).unwrap();
331+
assert_eq!(on_host.reported_version_package, Some("infra".to_string()));
332+
}
333+
334+
#[test]
335+
fn reported_version_package_required_when_multiple_packages() {
336+
let yaml = r#"
337+
packages:
338+
infra:
339+
download:
340+
oci:
341+
repository: newrelic-infra
342+
version: "1.2.3"
343+
flex:
344+
download:
345+
oci:
346+
repository: nri-flex
347+
version: "4.5.6"
348+
"#;
349+
let err = serde_saphyr::from_str::<OnHost>(yaml)
350+
.unwrap_err()
351+
.to_string();
352+
assert!(
353+
err.contains("reported_version_package") && err.contains("required"),
354+
"unexpected error: {err}"
355+
);
356+
assert!(
357+
err.contains("flex") && err.contains("infra"),
358+
"error should list declared package ids: {err}"
359+
);
360+
}
361+
362+
#[test]
363+
fn reported_version_package_referencing_unknown_id_errors() {
364+
let yaml = r#"
365+
reported_version_package: does-not-exist
366+
packages:
367+
infra:
368+
download:
369+
oci:
370+
repository: newrelic-infra
371+
version: "1.2.3"
372+
"#;
373+
let err = serde_saphyr::from_str::<OnHost>(yaml)
374+
.unwrap_err()
375+
.to_string();
376+
assert!(err.contains("unknown package"), "unexpected error: {err}");
377+
}
378+
379+
#[test]
380+
fn reported_version_package_set_without_packages_errors() {
381+
let yaml = "reported_version_package: infra\n";
382+
let err = serde_saphyr::from_str::<OnHost>(yaml)
383+
.unwrap_err()
384+
.to_string();
385+
assert!(err.contains("unknown package"), "unexpected error: {err}");
386+
}
387+
388+
#[test]
389+
fn no_packages_yields_no_reported_version_package() {
390+
let yaml = r#"
391+
executables:
392+
- id: test
393+
path: /bin/true
394+
args: []
395+
"#;
396+
let on_host: OnHost = serde_saphyr::from_str(yaml).unwrap();
397+
assert_eq!(on_host.reported_version_package, None);
398+
}
399+
236400
#[test]
237401
fn test_package_reserved_variable_dir_unknown_pkg_errors() {
238402
// Executable references a package not existing in the config
@@ -670,6 +834,7 @@ executables:
670834
filesystem: FileSystem::default(),
671835
shared_filesystem: SharedFileSystem::default(),
672836
packages: Default::default(),
837+
reported_version_package: None,
673838
};
674839

675840
// Compare the default OnHost instance with the parsed instance
@@ -748,6 +913,7 @@ executables:
748913
backoff_delay: 1s
749914
max_retries: 3
750915
last_retry_interval: 30s
916+
reported_version_package: otel-first
751917
packages:
752918
otel-first:
753919
download:

agent-control/src/agent_type/runtime_config/on_host/rendered.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub struct OnHost {
2525
pub shared_filesystem: SharedFileSystem,
2626
/// Packages to download for this agent.
2727
pub packages: RenderedPackages,
28+
/// Main OCI Package version reported as the `agent.version`.
29+
pub reported_version_package: Option<PackageID>,
2830
}
2931

3032
/// Rendered packages keyed by their [`PackageID`].

agent-control/src/sub_agent/on_host/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ where
165165
executables,
166166
on_host.health,
167167
on_host.packages,
168+
on_host.reported_version_package,
168169
self.package_manager.clone(),
169170
on_host.enable_file_logging,
170171
self.logging_path.to_path_buf(),

0 commit comments

Comments
 (0)