Skip to content

Commit b54d1c9

Browse files
authored
feat: add post_download_hook script support for agent types (#2573)
* feat: add post_download_hook script support for agent types
1 parent 1820435 commit b54d1c9

17 files changed

Lines changed: 645 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Remember that the keywords that you can use are the following:
1818
- Hardens service restart policies on Linux and Windows (systemd rate limiting: 5 restarts max in 60s) to prevent
1919
crash-looping from saturating CPU.
2020
- Added support for remote agent type retrieval.
21+
- add post-download script support for OCI packages.
2122

2223
## v1.17.0 - 2026-06-16
2324

agent-control/src/agent_control/version_updater/on_host.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ where
136136
version: new_version,
137137
public_key_url: Some(self.pub_key_url.clone()),
138138
},
139+
post_download_hook: None,
139140
}
140141
}
141142
}

agent-control/src/agent_type/definition.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ pub fn include_packages_variables(
328328
let package_data = PackageData {
329329
id: package_id.to_string(),
330330
oci: package.download.oci.clone(),
331+
post_download_hook: package.post_download_hook.clone(),
331332
};
332333
let path =
333334
get_package_path(Path::new(remote_dir), &agent_id, &package_data).map_err(|e| {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ mod tests {
169169
)),
170170
},
171171
},
172+
post_download_hook: None,
172173
};
173174

174175
let expected_packages = HashMap::from([

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,10 @@ impl Templateable for Executable {
3636
type Output = rendered::Executable;
3737

3838
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
39-
let args: Vec<String> = self
40-
.args
41-
.0
42-
.into_iter()
43-
.map(|arg| arg.template_with(variables))
44-
.collect::<Result<Vec<String>, AgentTypeError>>()?;
45-
4639
Ok(Self::Output {
4740
id: self.id.template_with(variables)?,
4841
path: self.path.template_with(variables)?,
49-
args: rendered::Args(args),
42+
args: self.args.template_with(variables)?,
5043
env: self.env.template_with(variables)?,
5144
restart_policy: self.restart_policy.template_with(variables)?,
5245
})
@@ -56,6 +49,18 @@ impl Templateable for Executable {
5649
#[derive(Debug, Default, Deserialize, Clone, PartialEq)]
5750
pub struct Args(pub Vec<TemplateableValue<String>>);
5851

52+
impl Templateable for Args {
53+
type Output = rendered::Args;
54+
55+
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
56+
self.0
57+
.into_iter()
58+
.map(|arg| arg.template_with(variables))
59+
.collect::<Result<Vec<String>, AgentTypeError>>()
60+
.map(rendered::Args)
61+
}
62+
}
63+
5964
#[derive(Debug, Default, Deserialize, Clone, PartialEq)]
6065
pub struct Env(pub(super) HashMap<String, TemplateableValue<String>>);
6166

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::str::FromStr;
22

33
use crate::agent_type::definition::Variables;
44
use crate::agent_type::error::AgentTypeError;
5+
use crate::agent_type::runtime_config::on_host::executable::{Args, Env};
56
use crate::agent_type::runtime_config::on_host::package::rendered::{Repository, Version};
67
use crate::agent_type::runtime_config::templateable_value::TemplateableValue;
78
use crate::agent_type::templates::Templateable;
@@ -14,6 +15,9 @@ pub mod rendered;
1415
pub(super) struct Package {
1516
/// Download defines the supported repository sources for the packages.
1617
pub download: Download,
18+
/// Post-download hook script to execute after downloading and extracting the package.
19+
/// All validations, checks, and installation steps should go here.
20+
pub post_download_hook: Option<PostDownloadHook>,
1721
}
1822

1923
pub type PackageID = String;
@@ -35,11 +39,37 @@ pub struct Oci {
3539
pub public_key_url: Option<TemplateableValue<String>>,
3640
}
3741

42+
#[derive(Debug, Deserialize, Clone, PartialEq)]
43+
pub struct PostDownloadHook {
44+
/// Path to the command/executable to run.
45+
/// Can be an absolute path (e.g., "/bin/bash") or a command name to search in PATH (e.g., "bash").
46+
/// Supports shell interpreters or direct binaries.
47+
pub path: TemplateableValue<String>,
48+
49+
/// Arguments passed to the command.
50+
/// - When using a shell: first arg is typically the script path, followed by script arguments
51+
/// Example: ["/path/to/install.sh", "--check-dependencies", "--verbose"]
52+
/// - When using a binary directly: arguments for that binary (can be empty)
53+
/// Example: ["--flag", "value"] or []
54+
#[serde(default)]
55+
pub args: Args,
56+
57+
/// Environmental variables passed to the process.
58+
#[serde(default)]
59+
pub env: Env,
60+
}
61+
3862
impl Templateable for Package {
3963
type Output = rendered::Package;
4064
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
65+
let post_download_hook = self
66+
.post_download_hook
67+
.map(|pd| pd.template_with(variables))
68+
.transpose()?;
69+
4170
Ok(Self::Output {
4271
download: self.download.template_with(variables)?,
72+
post_download_hook,
4373
})
4474
}
4575
}
@@ -86,6 +116,17 @@ impl Templateable for Oci {
86116
}
87117
}
88118

119+
impl Templateable for PostDownloadHook {
120+
type Output = rendered::PostDownloadHook;
121+
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
122+
let path = self.path.template_with(variables)?;
123+
let args = self.args.template_with(variables)?;
124+
let env = self.env.template_with(variables)?;
125+
126+
Ok(Self::Output { path, args, env })
127+
}
128+
}
129+
89130
#[cfg(test)]
90131
mod tests {
91132
use std::str::FromStr;
@@ -140,4 +181,58 @@ mod tests {
140181
assert_eq!(rendered_oci.version, Version::from_str(&version).unwrap());
141182
assert_eq!(rendered_oci.public_key_url, public_key_url);
142183
}
184+
185+
#[test]
186+
fn test_post_download_hook_template_with_variables() {
187+
use std::collections::HashMap;
188+
189+
let mut variables = Variables::new();
190+
variables.insert(
191+
"nr-var:version".to_string(),
192+
Variable::new_final_string_variable("1.0.0".to_string()),
193+
);
194+
variables.insert(
195+
"nr-var:script-path".to_string(),
196+
Variable::new_final_string_variable("/opt/install.sh".to_string()),
197+
);
198+
variables.insert(
199+
"nr-var:env-value".to_string(),
200+
Variable::new_final_string_variable("test-value".to_string()),
201+
);
202+
203+
let mut env_map = HashMap::new();
204+
env_map.insert(
205+
"AGENT_VERSION".to_string(),
206+
TemplateableValue::from_template("${nr-var:version}".to_string()),
207+
);
208+
env_map.insert(
209+
"CUSTOM_VAR".to_string(),
210+
TemplateableValue::from_template("${nr-var:env-value}".to_string()),
211+
);
212+
213+
let post_download_hook = PostDownloadHook {
214+
path: TemplateableValue::from_template("/bin/bash".to_string()),
215+
args: Args(vec![
216+
TemplateableValue::from_template("${nr-var:script-path}".to_string()),
217+
TemplateableValue::from_template("--version=${nr-var:version}".to_string()),
218+
]),
219+
env: Env(env_map),
220+
};
221+
222+
let rendered = post_download_hook.template_with(&variables).unwrap();
223+
224+
assert_eq!(rendered.path, "/bin/bash");
225+
assert_eq!(rendered.args.0.len(), 2);
226+
assert_eq!(rendered.args.0[0], "/opt/install.sh");
227+
assert_eq!(rendered.args.0[1], "--version=1.0.0");
228+
assert_eq!(rendered.env.0.len(), 2);
229+
assert_eq!(
230+
rendered.env.0.get("AGENT_VERSION"),
231+
Some(&"1.0.0".to_string())
232+
);
233+
assert_eq!(
234+
rendered.env.0.get("CUSTOM_VAR"),
235+
Some(&"test-value".to_string())
236+
);
237+
}
143238
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::{fmt::Display, str::FromStr};
2-
31
use oci_client::Reference;
42
use serde::{Deserialize, Serialize};
3+
use std::{fmt::Display, str::FromStr};
54
use url::Url;
65

76
use crate::agent_control::config::Registry;
7+
use crate::agent_type::runtime_config::on_host::executable::rendered::{Args, Env};
88

99
const REPOSITORY_TOTAL_LENGTH_MAX: usize = 255;
1010
const TAG_TOTAL_LENGTH_MAX: usize = 128;
@@ -13,6 +13,8 @@ const TAG_TOTAL_LENGTH_MAX: usize = 128;
1313
pub struct Package {
1414
/// Download defines the supported repository sources for the packages.
1515
pub download: Download,
16+
/// Post-download hook script to execute after downloading and extracting the package.
17+
pub post_download_hook: Option<PostDownloadHook>,
1618
}
1719

1820
#[derive(Debug, Clone, PartialEq)]
@@ -28,6 +30,16 @@ pub struct Oci {
2830
pub public_key_url: Option<Url>,
2931
}
3032

33+
#[derive(Debug, Clone, PartialEq)]
34+
pub struct PostDownloadHook {
35+
/// Absolute path to the command/executable (e.g., "/bin/bash", "/usr/bin/python3")
36+
pub path: String,
37+
/// Arguments passed to the executable on [`path`].
38+
pub args: Args,
39+
/// Environmental variables
40+
pub env: Env,
41+
}
42+
3143
const DEFAULT_TAG: &str = "latest";
3244

3345
impl Oci {

agent-control/src/package.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod manager;
22
pub mod oci;
3+
pub mod post_download_hook_executor;

agent-control/src/package/manager.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! This module manages package operations such as installation, removal, and updates.
22
use crate::agent_control::agent_id::AgentID;
3-
use crate::agent_type::runtime_config::on_host::package::rendered::Oci;
3+
use crate::agent_type::runtime_config::on_host::package::rendered::{Oci, PostDownloadHook};
44
use crate::package::oci::package_manager::OCIPackageManagerError;
55
use std::path::PathBuf;
66

@@ -9,6 +9,7 @@ use std::path::PathBuf;
99
pub struct PackageData {
1010
pub id: String, // same type as the packages map on an agent type definition
1111
pub oci: Oci,
12+
pub post_download_hook: Option<PostDownloadHook>,
1213
}
1314

1415
/// Information about an installed package

agent-control/src/package/oci/downloader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ pub mod tests {
168168
version: Version::from_str(VERSION).unwrap(),
169169
public_key_url,
170170
},
171+
post_download_hook: None,
171172
}
172173
}
173174

0 commit comments

Comments
 (0)