Skip to content

Commit 428cdc1

Browse files
feat: update OCI artifact downloader and media type handling (NR-510051) (#2070)
* refactor: update OCI artifact downloader and media type handling * review * refactored types
1 parent b4dc8e0 commit 428cdc1

File tree

19 files changed

+440
-391
lines changed

19 files changed

+440
-391
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::opamp::instance_id::storer::Storer;
2525
use crate::opamp::operations::build_opamp_with_channel;
2626
use crate::opamp::remote_config::validators::SupportedRemoteConfigValidator;
2727
use crate::opamp::remote_config::validators::regexes::RegexValidator;
28-
use crate::package::oci::downloader::OCIRefDownloader;
28+
use crate::package::oci::downloader::OCIArtifactDownloader;
2929
use crate::package::oci::package_manager::OCIPackageManager;
3030
use crate::secret_retriever::on_host::retrieve::OnHostSecretRetriever;
3131
use crate::secrets_provider::SecretsProviders;
@@ -165,9 +165,10 @@ impl AgentControlRunner {
165165
));
166166

167167
let packages_downloader =
168-
OCIRefDownloader::try_new(self.proxy, self.runtime, ClientConfig::default()).map_err(
169-
|err| RunError(format!("failed to create OCIRefDownloader client: {err}")),
170-
)?;
168+
OCIArtifactDownloader::try_new(self.proxy, self.runtime, ClientConfig::default())
169+
.map_err(|err| {
170+
RunError(format!("failed to create OCIRefDownloader client: {err}"))
171+
})?;
171172

172173
let package_manager = OCIPackageManager::new(
173174
packages_downloader,

agent-control/src/agent_type/error.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ pub enum AgentTypeError {
2525
RenderingTemplate(String),
2626
#[error("conflicting variable definition: {0}")]
2727
ConflictingVariableDefinition(String),
28-
#[error("unsupported package type: {0}")]
29-
UnsupportedPackageType(String),
3028
#[error("error parsing oci reference: {0}")]
3129
OCIReferenceParsingError(String),
3230
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ mod tests {
158158
);
159159

160160
let pkg = Package {
161-
package_type: TemplateableValue::from_template("tar.gz".to_string()),
162161
download: Download {
163162
oci: Oci {
164163
registry: TemplateableValue::from_template("${nr-var:registry}".to_string()),

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

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,8 @@ use std::str::FromStr;
88

99
pub mod rendered;
1010

11-
#[derive(Debug, Deserialize, Clone, PartialEq, Default)]
12-
pub enum PackageType {
13-
#[default]
14-
Tar,
15-
Zip,
16-
}
17-
18-
impl FromStr for PackageType {
19-
type Err = AgentTypeError;
20-
21-
fn from_str(s: &str) -> Result<Self, Self::Err> {
22-
match s {
23-
"tar" => Ok(Self::Tar),
24-
"tar.gz" => Ok(Self::Tar),
25-
"zip" => Ok(Self::Zip),
26-
_ => Err(AgentTypeError::UnsupportedPackageType(s.to_string())),
27-
}
28-
}
29-
}
30-
3111
#[derive(Debug, Deserialize, Default, Clone, PartialEq)]
3212
pub(super) struct Package {
33-
#[serde(rename = "type")]
34-
pub package_type: TemplateableValue<PackageType>, // Using `r#type` to avoid keyword conflict
35-
3613
/// Download defines the supported repository sources for the packages.
3714
pub download: Download,
3815
}
@@ -60,7 +37,6 @@ impl Templateable for Package {
6037
type Output = rendered::Package;
6138
fn template_with(self, variables: &Variables) -> Result<Self::Output, AgentTypeError> {
6239
Ok(Self::Output {
63-
package_type: self.package_type.template_with(variables)?,
6440
download: self.download.template_with(variables)?,
6541
})
6642
}
@@ -105,14 +81,6 @@ mod tests {
10581
use crate::agent_type::variable::Variable;
10682
use rstest::rstest;
10783

108-
#[test]
109-
fn test_package_type_from_str() {
110-
assert_eq!(PackageType::from_str("tar").unwrap(), PackageType::Tar);
111-
assert_eq!(PackageType::from_str("tar.gz").unwrap(), PackageType::Tar);
112-
assert_eq!(PackageType::from_str("zip").unwrap(), PackageType::Zip);
113-
assert!(PackageType::from_str("unsupported").is_err());
114-
}
115-
11684
#[rstest]
11785
#[case::only_digest(
11886
"@sha256:ec5f08ee7be8b557cd1fc5ae1a0ac985e8538da7c93f51a51eff4b277509a723",

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
use crate::agent_type::runtime_config::on_host::package::PackageType;
21
use oci_spec::distribution::Reference;
32

43
#[derive(Debug, Clone, PartialEq)]
54
pub struct Package {
6-
/// Package type (tar.gz, zip).
7-
pub package_type: PackageType,
8-
95
/// Download defines the supported repository sources for the packages.
106
pub download: Download,
117
}

agent-control/src/package.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
pub mod extract;
21
pub mod manager;
32
pub mod oci;

agent-control/src/package/manager.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
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::PackageType;
43
use crate::package::oci::package_manager::OCIPackageManagerError;
54
use oci_client::Reference;
65
use std::path::PathBuf;
@@ -9,7 +8,6 @@ use std::path::PathBuf;
98
#[derive(Debug, Clone)]
109
pub struct PackageData {
1110
pub id: String, // same type as the packages map on an agent type definition
12-
pub package_type: PackageType,
1311
pub oci_reference: Reference,
1412
}
1513

agent-control/src/package/oci.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod artifact_definitions;
12
pub mod downloader;
23
pub mod package_manager;
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
use std::fmt::Display;
2+
use std::path::{Path, PathBuf};
3+
4+
use crate::utils::extract::{extract_tar_gz, extract_zip};
5+
use oci_client::manifest::{OciDescriptor, OciImageManifest};
6+
7+
#[derive(Debug, thiserror::Error)]
8+
#[error("{0}")]
9+
pub struct DefinitionError(String);
10+
11+
const AGENT_PACKAGE_ARTIFACT_TYPE: &str = "application/vnd.newrelic.agent.v1";
12+
const AGENT_TYPE_ARTIFACT_TYPE: &str = "application/vnd.newrelic.agent-type.v1";
13+
/// OCI manifest artifact types supported.
14+
#[derive(Debug)]
15+
pub enum ManifestArtifactType {
16+
AgentPackage,
17+
AgentType,
18+
}
19+
impl TryFrom<&str> for ManifestArtifactType {
20+
type Error = DefinitionError;
21+
22+
fn try_from(artifact_type: &str) -> Result<Self, Self::Error> {
23+
match artifact_type {
24+
AGENT_PACKAGE_ARTIFACT_TYPE => Ok(ManifestArtifactType::AgentPackage),
25+
AGENT_TYPE_ARTIFACT_TYPE => Ok(ManifestArtifactType::AgentType),
26+
other => Err(DefinitionError(format!(
27+
"unsupported artifact type: {}",
28+
other
29+
))),
30+
}
31+
}
32+
}
33+
impl Display for ManifestArtifactType {
34+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35+
match self {
36+
ManifestArtifactType::AgentPackage => write!(f, "{}", AGENT_PACKAGE_ARTIFACT_TYPE),
37+
ManifestArtifactType::AgentType => write!(f, "{}", AGENT_TYPE_ARTIFACT_TYPE),
38+
}
39+
}
40+
}
41+
42+
const AGENT_PACKAGE_LAYER_TAR_GZ: &str = "application/vnd.newrelic.agent.content.v1.tar+gzip";
43+
const AGENT_PACKAGE_LAYER_ZIP: &str = "application/vnd.newrelic.agent.content.v1.zip";
44+
const AGENT_TYPE_LAYER_TAR_GZ: &str = "application/vnd.newrelic.agent-type.content.v1.tar+gzip";
45+
46+
/// OCI layer media types. Having the Other variant allows for future extensibility,
47+
/// allowing us to fetch and use artifacts with unknown layers if needed.
48+
#[derive(Debug)]
49+
pub enum LayerMediaType {
50+
AgentPackage(PackageMediaType),
51+
AgentType,
52+
Other(String),
53+
}
54+
impl From<&str> for LayerMediaType {
55+
fn from(media_type: &str) -> Self {
56+
match media_type {
57+
AGENT_PACKAGE_LAYER_TAR_GZ => {
58+
LayerMediaType::AgentPackage(PackageMediaType::AgentPackageLayerTarGz)
59+
}
60+
AGENT_PACKAGE_LAYER_ZIP => {
61+
LayerMediaType::AgentPackage(PackageMediaType::AgentPackageLayerZip)
62+
}
63+
AGENT_TYPE_LAYER_TAR_GZ => LayerMediaType::AgentType,
64+
other => LayerMediaType::Other(other.to_string()),
65+
}
66+
}
67+
}
68+
impl Display for LayerMediaType {
69+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70+
match self {
71+
LayerMediaType::AgentPackage(pkg_media_type) => match pkg_media_type {
72+
PackageMediaType::AgentPackageLayerTarGz => {
73+
write!(f, "{}", AGENT_PACKAGE_LAYER_TAR_GZ)
74+
}
75+
PackageMediaType::AgentPackageLayerZip => {
76+
write!(f, "{}", AGENT_PACKAGE_LAYER_ZIP)
77+
}
78+
},
79+
LayerMediaType::AgentType => write!(f, "{}", AGENT_TYPE_LAYER_TAR_GZ),
80+
LayerMediaType::Other(other) => write!(f, "{}", other),
81+
}
82+
}
83+
}
84+
85+
#[derive(Debug)]
86+
pub enum PackageMediaType {
87+
AgentPackageLayerTarGz,
88+
AgentPackageLayerZip,
89+
}
90+
impl Display for PackageMediaType {
91+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92+
match self {
93+
PackageMediaType::AgentPackageLayerTarGz => write!(f, "{}", AGENT_PACKAGE_LAYER_TAR_GZ),
94+
PackageMediaType::AgentPackageLayerZip => write!(f, "{}", AGENT_PACKAGE_LAYER_ZIP),
95+
}
96+
}
97+
}
98+
99+
/// Represents a Agent Package OCI artifact locally stored.
100+
#[derive(Debug)]
101+
pub struct LocalAgentPackage {
102+
blob_path: PathBuf,
103+
blob_media_type: PackageMediaType,
104+
}
105+
impl LocalAgentPackage {
106+
pub fn new(blob_media_type: PackageMediaType, blob_path: PathBuf) -> Self {
107+
Self {
108+
blob_media_type,
109+
blob_path,
110+
}
111+
}
112+
113+
/// Extracts the agent package to the specified destination path.
114+
pub fn extract(&self, dest_path: &Path) -> Result<(), DefinitionError> {
115+
match &self.blob_media_type {
116+
PackageMediaType::AgentPackageLayerTarGz => extract_tar_gz(&self.blob_path, dest_path),
117+
PackageMediaType::AgentPackageLayerZip => extract_zip(&self.blob_path, dest_path),
118+
}
119+
.map_err(|e| DefinitionError(format!("failed extracting: {e}")))
120+
}
121+
122+
/// Validates that the manifest meets the requirements for an Agent Package artifact and
123+
/// returns the layer descriptor that contains the package blob with its media type.
124+
/// Agent Package Manifest requirements:
125+
/// - artifactType must be '[AGENT_PACKAGE_ARTIFACT_TYPE]'
126+
/// - exactly one layer with mediaType of '[PackageMediaType]'
127+
pub fn get_layer(
128+
manifest: &OciImageManifest,
129+
) -> Result<(OciDescriptor, PackageMediaType), DefinitionError> {
130+
if manifest.artifact_type.as_deref() != Some(AGENT_PACKAGE_ARTIFACT_TYPE) {
131+
return Err(DefinitionError(format!(
132+
"invalid artifactType: expected {}, got {:?}",
133+
AGENT_PACKAGE_ARTIFACT_TYPE, manifest.artifact_type
134+
)));
135+
}
136+
let mut supported_layers = manifest.layers.iter().filter_map(|layer| {
137+
match LayerMediaType::from(layer.media_type.as_str()) {
138+
LayerMediaType::AgentPackage(pkg_media_type) => Some((layer, pkg_media_type)),
139+
_ => None,
140+
}
141+
});
142+
143+
let Some((layer, media_type)) = supported_layers.next() else {
144+
return Err(DefinitionError(format!(
145+
"agent package artifact must have at least one supported layer {} or {}",
146+
PackageMediaType::AgentPackageLayerTarGz,
147+
PackageMediaType::AgentPackageLayerZip
148+
)));
149+
};
150+
if supported_layers.next().is_some() {
151+
return Err(DefinitionError(
152+
"agent package artifact must have exactly one supported layer".to_string(),
153+
));
154+
}
155+
Ok((layer.clone(), media_type))
156+
}
157+
}
158+
159+
#[cfg(test)]
160+
pub mod tests {
161+
use super::*;
162+
use assert_matches::assert_matches;
163+
164+
#[rstest::rstest]
165+
#[case::tar_gz_single_layer(
166+
vec![AGENT_PACKAGE_LAYER_TAR_GZ]
167+
)]
168+
#[case::zip_single_layer(
169+
vec![AGENT_PACKAGE_LAYER_ZIP]
170+
)]
171+
#[case::tar_gz_with_extra_layers(
172+
vec![AGENT_PACKAGE_LAYER_TAR_GZ, "application/vnd.custom.extra.v1"]
173+
)]
174+
#[case::zip_with_extra_layers(
175+
vec![AGENT_PACKAGE_LAYER_ZIP, "application/vnd.custom.extra.v1"]
176+
)]
177+
fn test_local_artifact_to_agent_package_success(#[case] layer_media_types: Vec<&str>) {
178+
let layers = layer_media_types
179+
.iter()
180+
.map(|media_type| OciDescriptor {
181+
media_type: media_type.to_string(),
182+
..Default::default()
183+
})
184+
.collect();
185+
let manifest = OciImageManifest {
186+
artifact_type: Some(ManifestArtifactType::AgentPackage.to_string()),
187+
layers,
188+
..Default::default()
189+
};
190+
191+
let (_, media_type) = LocalAgentPackage::get_layer(&manifest).unwrap();
192+
match layer_media_types[0] {
193+
AGENT_PACKAGE_LAYER_TAR_GZ => {
194+
assert_matches!(media_type, PackageMediaType::AgentPackageLayerTarGz)
195+
}
196+
AGENT_PACKAGE_LAYER_ZIP => {
197+
assert_matches!(media_type, PackageMediaType::AgentPackageLayerZip)
198+
}
199+
_ => panic!("unexpected media type"),
200+
}
201+
}
202+
#[rstest::rstest]
203+
#[case::invalid_artifact_type(
204+
"application/vnd.newrelic.unknown.v1",
205+
vec![],
206+
"invalid artifactType"
207+
)]
208+
#[case::no_supported_layers(
209+
AGENT_PACKAGE_ARTIFACT_TYPE,
210+
vec!["application/vnd.custom.extra.v1"],
211+
"must have at least one supported layer"
212+
)]
213+
#[case::empty_layers(
214+
AGENT_PACKAGE_ARTIFACT_TYPE,
215+
vec![],
216+
"must have at least one supported layer"
217+
)]
218+
#[case::multiple_supported_layers(
219+
AGENT_PACKAGE_ARTIFACT_TYPE,
220+
vec![AGENT_PACKAGE_LAYER_TAR_GZ, AGENT_PACKAGE_LAYER_ZIP],
221+
"must have exactly one supported layer"
222+
)]
223+
fn test_local_artifact_to_agent_package_failure(
224+
#[case] artifact_type: &str,
225+
#[case] layer_media_types: Vec<&str>,
226+
#[case] expected_error: &str,
227+
) {
228+
let layers = layer_media_types
229+
.iter()
230+
.map(|media_type| OciDescriptor {
231+
media_type: media_type.to_string(),
232+
..Default::default()
233+
})
234+
.collect();
235+
let manifest = OciImageManifest {
236+
artifact_type: Some(artifact_type.to_string()),
237+
layers,
238+
..Default::default()
239+
};
240+
let err = LocalAgentPackage::get_layer(&manifest).unwrap_err();
241+
assert!(err.to_string().contains(expected_error));
242+
}
243+
}

0 commit comments

Comments
 (0)