|
| 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