use serde::{Deserialize, Serialize};
use crate::constants::API_VERSION;
use crate::resources::ObjectMetadata;
use crate::{NylError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteManifestSpec {
pub url: String,
#[serde(default, rename = "overrideNamespace", skip_serializing_if = "std::ops::Not::not")]
pub override_namespace: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteManifest {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: ObjectMetadata,
pub spec: RemoteManifestSpec,
}
impl RemoteManifest {
pub fn is_remote_manifest(manifest: &serde_json::Value) -> bool {
manifest.get("apiVersion").and_then(|v| v.as_str()) == Some(API_VERSION)
&& manifest.get("kind").and_then(|v| v.as_str()) == Some("RemoteManifest")
}
pub fn from_value(value: &serde_json::Value) -> Result<Self> {
serde_json::from_value(value.clone())
.map_err(|e| NylError::Config(format!("Invalid RemoteManifest resource: {}", e)))
}
pub fn validate(&self) -> Result<()> {
let url = self.spec.url.trim();
if url.is_empty() {
return Err(NylError::Config(
"RemoteManifest spec.url must not be empty".to_string(),
));
}
if !url.starts_with("https://") {
return Err(NylError::Config(format!(
"RemoteManifest spec.url must use https:// scheme, got '{}'",
self.spec.url
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_is_remote_manifest_true() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {"url": "https://example.com/manifests.yaml"}
});
assert!(RemoteManifest::is_remote_manifest(&manifest));
}
#[test]
fn test_is_remote_manifest_false() {
let manifest = json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {"name": "cm"}
});
assert!(!RemoteManifest::is_remote_manifest(&manifest));
}
#[test]
fn test_remote_manifest_validate_rejects_empty_url() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {"url": " "}
});
let remote = RemoteManifest::from_value(&manifest).unwrap();
let err = remote.validate().unwrap_err();
assert!(err.to_string().contains("must not be empty"));
}
#[test]
fn test_remote_manifest_validate_rejects_non_https_url() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {"url": "http://example.com/manifests.yaml"}
});
let remote = RemoteManifest::from_value(&manifest).unwrap();
let err = remote.validate().unwrap_err();
assert!(err.to_string().contains("https://"));
}
#[test]
fn test_remote_manifest_override_namespace_defaults_to_false() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {"url": "https://example.com/manifests.yaml"}
});
let remote = RemoteManifest::from_value(&manifest).unwrap();
assert!(!remote.spec.override_namespace);
}
#[test]
fn test_remote_manifest_override_namespace_true() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {
"url": "https://example.com/manifests.yaml",
"overrideNamespace": true
}
});
let remote = RemoteManifest::from_value(&manifest).unwrap();
assert!(remote.spec.override_namespace);
}
#[test]
fn test_remote_manifest_from_value_rejects_unknown_fields() {
let manifest = json!({
"apiVersion": "nyl.niklasrosenstein.github.com/v1",
"kind": "RemoteManifest",
"metadata": {"name": "remote"},
"spec": {"url": "https://example.com/manifests.yaml", "unknownField": true}
});
let err = RemoteManifest::from_value(&manifest).unwrap_err();
assert!(err.to_string().contains("unknown field"));
}
}