Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably document the new functionality on INTEGRATING_AGENTS.md.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Remember that the keywords that you can use are the following:
- Hardens service restart policies on Linux and Windows (systemd rate limiting: 5 restarts max in 60s) to prevent
crash-looping from saturating CPU.
- Added support for remote agent type retrieval.
- add post-download script support for OCI packages.

## v1.17.0 - 2026-06-16

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ where
version: new_version,
public_key_url: Some(self.pub_key_url.clone()),
},
post_download_hook: None,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions agent-control/src/agent_type/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ pub fn include_packages_variables(
let package_data = PackageData {
id: package_id.to_string(),
oci: package.download.oci.clone(),
post_download_hook: package.post_download_hook.clone(),
};
let path =
get_package_path(Path::new(remote_dir), &agent_id, &package_data).map_err(|e| {
Expand Down
1 change: 1 addition & 0 deletions agent-control/src/agent_type/runtime_config/on_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ mod tests {
)),
},
},
post_download_hook: None,
};

let expected_packages = HashMap::from([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,10 @@ impl Templateable for Executable {
type Output = rendered::Executable;

fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
let args: Vec<String> = self
.args
.0
.into_iter()
.map(|arg| arg.template_with(variables))
.collect::<Result<Vec<String>, AgentTypeError>>()?;

Ok(Self::Output {
id: self.id.template_with(variables)?,
path: self.path.template_with(variables)?,
args: rendered::Args(args),
args: self.args.template_with(variables)?,
env: self.env.template_with(variables)?,
restart_policy: self.restart_policy.template_with(variables)?,
})
Expand All @@ -56,6 +49,18 @@ impl Templateable for Executable {
#[derive(Debug, Default, Deserialize, Clone, PartialEq)]
pub struct Args(pub Vec<TemplateableValue<String>>);

impl Templateable for Args {
type Output = rendered::Args;

fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
self.0
.into_iter()
.map(|arg| arg.template_with(variables))
.collect::<Result<Vec<String>, AgentTypeError>>()
.map(rendered::Args)
}
}

#[derive(Debug, Default, Deserialize, Clone, PartialEq)]
pub struct Env(pub(super) HashMap<String, TemplateableValue<String>>);

Expand Down
95 changes: 95 additions & 0 deletions agent-control/src/agent_type/runtime_config/on_host/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::str::FromStr;

use crate::agent_type::definition::Variables;
use crate::agent_type::error::AgentTypeError;
use crate::agent_type::runtime_config::on_host::executable::{Args, Env};
use crate::agent_type::runtime_config::on_host::package::rendered::{Repository, Version};
use crate::agent_type::runtime_config::templateable_value::TemplateableValue;
use crate::agent_type::templates::Templateable;
Expand All @@ -14,6 +15,9 @@ pub mod rendered;
pub(super) struct Package {
/// Download defines the supported repository sources for the packages.
pub download: Download,
/// Post-download hook script to execute after downloading and extracting the package.
/// All validations, checks, and installation steps should go here.
Comment thread
danielorihuela marked this conversation as resolved.
pub post_download_hook: Option<PostDownloadHook>,
}

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

#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct PostDownloadHook {
/// Path to the command/executable to run.
/// Can be an absolute path (e.g., "/bin/bash") or a command name to search in PATH (e.g., "bash").
/// Supports shell interpreters or direct binaries.
pub path: TemplateableValue<String>,

/// Arguments passed to the command.
/// - When using a shell: first arg is typically the script path, followed by script arguments
/// Example: ["/path/to/install.sh", "--check-dependencies", "--verbose"]
/// - When using a binary directly: arguments for that binary (can be empty)
/// Example: ["--flag", "value"] or []
#[serde(default)]
pub args: Args,

/// Environmental variables passed to the process.
#[serde(default)]
pub env: Env,
}

impl Templateable for Package {
type Output = rendered::Package;
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
let post_download_hook = self
.post_download_hook
.map(|pd| pd.template_with(variables))
.transpose()?;

Ok(Self::Output {
download: self.download.template_with(variables)?,
post_download_hook,
})
}
}
Expand Down Expand Up @@ -86,6 +116,17 @@ impl Templateable for Oci {
}
}

impl Templateable for PostDownloadHook {
type Output = rendered::PostDownloadHook;
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
let path = self.path.template_with(variables)?;
let args = self.args.template_with(variables)?;
let env = self.env.template_with(variables)?;

Ok(Self::Output { path, args, env })
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down Expand Up @@ -140,4 +181,58 @@ mod tests {
assert_eq!(rendered_oci.version, Version::from_str(&version).unwrap());
assert_eq!(rendered_oci.public_key_url, public_key_url);
}

#[test]
fn test_post_download_hook_template_with_variables() {
use std::collections::HashMap;

let mut variables = Variables::new();
variables.insert(
"nr-var:version".to_string(),
Variable::new_final_string_variable("1.0.0".to_string()),
);
variables.insert(
"nr-var:script-path".to_string(),
Variable::new_final_string_variable("/opt/install.sh".to_string()),
);
variables.insert(
"nr-var:env-value".to_string(),
Variable::new_final_string_variable("test-value".to_string()),
);

let mut env_map = HashMap::new();
env_map.insert(
"AGENT_VERSION".to_string(),
TemplateableValue::from_template("${nr-var:version}".to_string()),
);
env_map.insert(
"CUSTOM_VAR".to_string(),
TemplateableValue::from_template("${nr-var:env-value}".to_string()),
);

let post_download_hook = PostDownloadHook {
path: TemplateableValue::from_template("/bin/bash".to_string()),
args: Args(vec![
TemplateableValue::from_template("${nr-var:script-path}".to_string()),
TemplateableValue::from_template("--version=${nr-var:version}".to_string()),
]),
env: Env(env_map),
};

let rendered = post_download_hook.template_with(&variables).unwrap();

assert_eq!(rendered.path, "/bin/bash");
assert_eq!(rendered.args.0.len(), 2);
assert_eq!(rendered.args.0[0], "/opt/install.sh");
assert_eq!(rendered.args.0[1], "--version=1.0.0");
assert_eq!(rendered.env.0.len(), 2);
assert_eq!(
rendered.env.0.get("AGENT_VERSION"),
Some(&"1.0.0".to_string())
);
assert_eq!(
rendered.env.0.get("CUSTOM_VAR"),
Some(&"test-value".to_string())
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{fmt::Display, str::FromStr};

use oci_client::Reference;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, str::FromStr};
use url::Url;

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

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

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

#[derive(Debug, Clone, PartialEq)]
pub struct PostDownloadHook {
/// Absolute path to the command/executable (e.g., "/bin/bash", "/usr/bin/python3")
pub path: String,
/// Arguments passed to the executable on [`path`].
pub args: Args,
/// Environmental variables
pub env: Env,
}

const DEFAULT_TAG: &str = "latest";

impl Oci {
Expand Down
1 change: 1 addition & 0 deletions agent-control/src/package.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod manager;
pub mod oci;
pub mod post_download_hook_executor;
3 changes: 2 additions & 1 deletion agent-control/src/package/manager.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! This module manages package operations such as installation, removal, and updates.
use crate::agent_control::agent_id::AgentID;
use crate::agent_type::runtime_config::on_host::package::rendered::Oci;
use crate::agent_type::runtime_config::on_host::package::rendered::{Oci, PostDownloadHook};
use crate::package::oci::package_manager::OCIPackageManagerError;
use std::path::PathBuf;

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

/// Information about an installed package
Expand Down
1 change: 1 addition & 0 deletions agent-control/src/package/oci/downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub mod tests {
version: Version::from_str(VERSION).unwrap(),
public_key_url,
},
post_download_hook: None,
}
}

Expand Down
17 changes: 17 additions & 0 deletions agent-control/src/package/oci/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ use crate::oci::OciClientError;
use crate::oci::artifact_definitions::LocalAgentPackage;
use crate::package::manager::{InstalledPackageData, PackageData, PackageManager};
use crate::package::oci::downloader::OCIPackageArtifactDownloader;
use crate::package::post_download_hook_executor::{
PostDownloadHookExecutionError, PostDownloadHookExecutor,
};
use fs::directory_manager::{DirectoryManager, DirectoryManagerFs};
use fs::file::LocalFile;
use fs::file::reader::FileReader;
Expand Down Expand Up @@ -46,6 +49,8 @@ pub enum OCIPackageManagerError {
NotNormalSuffix(String),
#[error("errors removing packages: {0}")]
RetainPackageErrors(RetainPackageErrors),
#[error("post-download hook execution failed: {0}")]
PostDownloadHook(#[from] PostDownloadHookExecutionError),
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -108,6 +113,16 @@ where
self.extract_package(&downloaded_package, install_path)
.inspect_err(|e| warn!("OCI package installation failed: {}", e))?;

// Execute post-download hook if configured
if let Some(ref hook) = package_data.post_download_hook {
debug!(
"Executing post-download hook for package {}",
package_data.id
);
let executor = PostDownloadHookExecutor::new(install_path.to_path_buf());
executor.execute(hook)?;
}

debug!("OCI package installed at {}", install_path.display());
Ok(InstalledPackageData {
id: package_data.id.clone(),
Expand Down Expand Up @@ -404,6 +419,7 @@ mod tests {
.unwrap(),
public_key_url: None,
},
post_download_hook: None,
}
}

Expand All @@ -419,6 +435,7 @@ mod tests {
version: Version::from_str(version).unwrap(),
public_key_url: None,
},
post_download_hook: None,
}
}

Expand Down
Loading
Loading