wadm_types/
validation.rs

1//! Logic for model ([`Manifest`]) validation
2//!
3
4use std::collections::{HashMap, HashSet};
5#[cfg(not(target_family = "wasm"))]
6use std::path::Path;
7use std::sync::OnceLock;
8
9use anyhow::{Context as _, Result};
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    CapabilityProperties, ComponentProperties, LinkProperty, Manifest, Properties, Trait,
15    TraitProperty, DEFAULT_LINK_NAME, LATEST_VERSION,
16};
17
18/// A namespace -> package -> interface lookup
19type KnownInterfaceLookup = HashMap<String, HashMap<String, HashMap<String, ()>>>;
20
21/// Hard-coded list of known namespaces/packages and the interfaces they contain.
22///
23/// Using an interface that is *not* on this list is not an error --
24/// custom interfaces are expected to not be on this list, but when using
25/// a known namespace and package, interfaces should generally be well known.
26static KNOWN_INTERFACE_LOOKUP: OnceLock<KnownInterfaceLookup> = OnceLock::new();
27
28const SECRET_POLICY_TYPE: &str = "policy.secret.wasmcloud.dev/v1alpha1";
29
30/// Get the static list of known interfaces
31fn get_known_interface_lookup() -> &'static KnownInterfaceLookup {
32    KNOWN_INTERFACE_LOOKUP.get_or_init(|| {
33        HashMap::from([
34            (
35                "wrpc".into(),
36                HashMap::from([
37                    (
38                        "blobstore".into(),
39                        HashMap::from([("blobstore".into(), ())]),
40                    ),
41                    (
42                        "keyvalue".into(),
43                        HashMap::from([("atomics".into(), ()), ("store".into(), ())]),
44                    ),
45                    (
46                        "http".into(),
47                        HashMap::from([
48                            ("incoming-handler".into(), ()),
49                            ("outgoing-handler".into(), ()),
50                        ]),
51                    ),
52                ]),
53            ),
54            (
55                "wasi".into(),
56                HashMap::from([
57                    (
58                        "blobstore".into(),
59                        HashMap::from([("blobstore".into(), ())]),
60                    ),
61                    ("config".into(), HashMap::from([("runtime".into(), ())])),
62                    (
63                        "keyvalue".into(),
64                        HashMap::from([
65                            ("atomics".into(), ()),
66                            ("store".into(), ()),
67                            ("batch".into(), ()),
68                            ("watch".into(), ()),
69                        ]),
70                    ),
71                    (
72                        "http".into(),
73                        HashMap::from([
74                            ("incoming-handler".into(), ()),
75                            ("outgoing-handler".into(), ()),
76                        ]),
77                    ),
78                    ("logging".into(), HashMap::from([("logging".into(), ())])),
79                ]),
80            ),
81            (
82                "wasmcloud".into(),
83                HashMap::from([(
84                    "messaging".into(),
85                    HashMap::from([("consumer".into(), ()), ("handler".into(), ())]),
86                )]),
87            ),
88        ])
89    })
90}
91
92static MANIFEST_NAME_REGEX_STR: &str = r"^[-\w]+$";
93static MANIFEST_NAME_REGEX: OnceLock<Regex> = OnceLock::new();
94
95/// Retrieve regular expression which manifest names must match, compiled to a usable [`Regex`]
96fn get_manifest_name_regex() -> &'static Regex {
97    MANIFEST_NAME_REGEX.get_or_init(|| {
98        Regex::new(MANIFEST_NAME_REGEX_STR)
99            .context("failed to parse manifest name regex")
100            .unwrap()
101    })
102}
103
104/// Check whether a manifest name matches requirements, returning all validation errors
105pub fn validate_manifest_name(name: &str) -> impl ValidationOutput {
106    let mut errors = Vec::new();
107    if !get_manifest_name_regex().is_match(name) {
108        errors.push(ValidationFailure::new(
109            ValidationFailureLevel::Error,
110            format!("manifest name [{name}] is not allowed (should match regex [{MANIFEST_NAME_REGEX_STR}])"),
111        ))
112    }
113    errors
114}
115
116/// Check whether a manifest name matches requirements
117pub fn is_valid_manifest_name(name: &str) -> bool {
118    validate_manifest_name(name).valid()
119}
120
121/// Check whether a manifest version is valid, returning all validation errors
122pub fn validate_manifest_version(version: &str) -> impl ValidationOutput {
123    let mut errors = Vec::new();
124    if version == LATEST_VERSION {
125        errors.push(ValidationFailure::new(
126            ValidationFailureLevel::Error,
127            format!("{LATEST_VERSION} is not allowed in wadm"),
128        ))
129    }
130    errors
131}
132
133/// Check whether a manifest version is valid requirements
134pub fn is_valid_manifest_version(version: &str) -> bool {
135    validate_manifest_version(version).valid()
136}
137
138/// Check whether a known grouping of namespace, package and interface are valid.
139/// A grouping must be both known/expected and invalid to fail this test (ex. a typo).
140///
141/// NOTE: what is considered a valid interface known to the host depends explicitly on
142/// the wasmCloud host and wasmCloud project goals/implementation. This information is
143/// subject to change.
144fn is_invalid_known_interface(
145    namespace: &str,
146    package: &str,
147    interface: &str,
148) -> Vec<ValidationFailure> {
149    let known_interfaces = get_known_interface_lookup();
150    let Some(pkg_lookup) = known_interfaces.get(namespace) else {
151        // This namespace isn't known, so it may be a custom interface
152        return vec![];
153    };
154    let Some(iface_lookup) = pkg_lookup.get(package) else {
155        // Unknown package inside a known interface we control is probably a bug
156        return vec![ValidationFailure::new(
157            ValidationFailureLevel::Warning,
158            format!("unrecognized interface [{namespace}:{package}/{interface}]"),
159        )];
160    };
161    // Unknown interface inside known namespace and package is probably a bug
162    if !iface_lookup.contains_key(interface) {
163        // Unknown package inside a known interface we control is probably a bug, but may be
164        // a new interface we don't know about yet
165        return vec![ValidationFailure::new(
166            ValidationFailureLevel::Warning,
167            format!("unrecognized interface [{namespace}:{package}/{interface}]"),
168        )];
169    }
170
171    Vec::new()
172}
173
174/// Level of a failure related to validation
175#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
176#[non_exhaustive]
177pub enum ValidationFailureLevel {
178    #[default]
179    Warning,
180    Error,
181}
182
183impl core::fmt::Display for ValidationFailureLevel {
184    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185        write!(
186            f,
187            "{}",
188            match self {
189                Self::Warning => "warning",
190                Self::Error => "error",
191            }
192        )
193    }
194}
195
196/// Failure detailing a validation failure, normally indicating a failure
197#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
198#[non_exhaustive]
199pub struct ValidationFailure {
200    pub level: ValidationFailureLevel,
201    pub msg: String,
202}
203
204impl ValidationFailure {
205    fn new(level: ValidationFailureLevel, msg: String) -> Self {
206        ValidationFailure { level, msg }
207    }
208}
209
210impl core::fmt::Display for ValidationFailure {
211    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212        write!(f, "[{}] {}", self.level, self.msg)
213    }
214}
215
216/// Things that support output validation
217pub trait ValidationOutput {
218    /// Whether the object is valid
219    fn valid(&self) -> bool;
220    /// Warnings returned (if any) during validation
221    fn warnings(&self) -> Vec<&ValidationFailure>;
222    /// The errors returned by the validation
223    fn errors(&self) -> Vec<&ValidationFailure>;
224}
225
226/// Default implementation for a list of concrete [`ValidationFailure`]s
227impl ValidationOutput for [ValidationFailure] {
228    fn valid(&self) -> bool {
229        self.errors().is_empty()
230    }
231    fn warnings(&self) -> Vec<&ValidationFailure> {
232        self.iter()
233            .filter(|m| m.level == ValidationFailureLevel::Warning)
234            .collect()
235    }
236    fn errors(&self) -> Vec<&ValidationFailure> {
237        self.iter()
238            .filter(|m| m.level == ValidationFailureLevel::Error)
239            .collect()
240    }
241}
242
243/// Default implementation for a list of concrete [`ValidationFailure`]s
244impl ValidationOutput for Vec<ValidationFailure> {
245    fn valid(&self) -> bool {
246        self.as_slice().valid()
247    }
248    fn warnings(&self) -> Vec<&ValidationFailure> {
249        self.iter()
250            .filter(|m| m.level == ValidationFailureLevel::Warning)
251            .collect()
252    }
253    fn errors(&self) -> Vec<&ValidationFailure> {
254        self.iter()
255            .filter(|m| m.level == ValidationFailureLevel::Error)
256            .collect()
257    }
258}
259
260/// Validate a WADM application manifest, returning a list of validation failures
261///
262/// At present this can check for:
263/// - unsupported interfaces (i.e. typos, etc)
264/// - unknown packages under known namespaces
265/// - "dangling" links (missing components)
266///
267/// Since `[ValidationFailure]` implements `ValidationOutput`, you can call `valid()` and other
268/// trait methods on it:
269///
270/// ```rust,ignore
271/// let messages = validate_manifest(some_path).await?;
272/// let valid = messages.valid();
273/// ```
274///
275/// # Arguments
276///
277/// * `path` - Path to the Manifest that will be read into memory and validated
278#[cfg(not(target_family = "wasm"))]
279pub async fn validate_manifest_file(
280    path: impl AsRef<Path>,
281) -> Result<(Manifest, Vec<ValidationFailure>)> {
282    let content = tokio::fs::read_to_string(path.as_ref())
283        .await
284        .with_context(|| format!("failed to read manifest @ [{}]", path.as_ref().display()))?;
285
286    validate_manifest_bytes(&content).await.with_context(|| {
287        format!(
288            "failed to parse YAML manifest [{}]",
289            path.as_ref().display()
290        )
291    })
292}
293
294/// Validate a lsit of bytes that represents a  WADM application manifest
295///
296/// # Arguments
297///
298/// * `content` - YAML content  to the Manifest that will be read into memory and validated
299pub async fn validate_manifest_bytes(
300    content: impl AsRef<[u8]>,
301) -> Result<(Manifest, Vec<ValidationFailure>)> {
302    let raw_yaml_content = content.as_ref();
303    let manifest =
304        serde_yaml::from_slice(content.as_ref()).context("failed to parse manifest content")?;
305    let mut failures = validate_manifest(&manifest).await?;
306    let mut yaml_issues = validate_raw_yaml(raw_yaml_content)?;
307    failures.append(&mut yaml_issues);
308    Ok((manifest, failures))
309}
310
311/// Validate a WADM application manifest, returning a list of validation failures
312///
313/// At present this can check for:
314/// - unsupported interfaces (i.e. typos, etc)
315/// - unknown packages under known namespaces
316/// - "dangling" links (missing components)
317/// - secrets mapped to unknown policies
318///
319/// Since `[ValidationFailure]` implements `ValidationOutput`, you can call `valid()` and other
320/// trait methods on it:
321///
322/// ```rust,ignore
323/// let messages = validate_manifest(some_path).await?;
324/// let valid = messages.valid();
325/// ```
326///
327/// # Arguments
328///
329/// * `manifest` - The [`Manifest`] that should be validated
330pub async fn validate_manifest(manifest: &Manifest) -> Result<Vec<ValidationFailure>> {
331    // Check for known failures with the manifest
332    let mut failures = Vec::new();
333    failures.extend(
334        validate_manifest_name(&manifest.metadata.name)
335            .errors()
336            .into_iter()
337            .cloned(),
338    );
339    failures.extend(
340        validate_manifest_version(manifest.version())
341            .errors()
342            .into_iter()
343            .cloned(),
344    );
345    failures.extend(core_validation(manifest));
346    failures.extend(check_misnamed_interfaces(manifest));
347    failures.extend(check_dangling_links(manifest));
348    failures.extend(validate_policies(manifest));
349    failures.extend(ensure_no_custom_traits(manifest));
350    failures.extend(validate_component_properties(manifest));
351    failures.extend(check_duplicate_links(manifest));
352    failures.extend(validate_link_configs(manifest));
353    Ok(failures)
354}
355
356pub fn validate_raw_yaml(content: &[u8]) -> Result<Vec<ValidationFailure>> {
357    let mut failures = Vec::new();
358    let raw_content: serde_yaml::Value =
359        serde_yaml::from_slice(content).context("failed read raw yaml content")?;
360    failures.extend(validate_components_configs(&raw_content));
361    Ok(failures)
362}
363
364fn core_validation(manifest: &Manifest) -> Vec<ValidationFailure> {
365    let mut failures = Vec::new();
366    let mut name_registry: HashSet<String> = HashSet::new();
367    let mut id_registry: HashSet<String> = HashSet::new();
368    let mut required_capability_components: HashSet<String> = HashSet::new();
369
370    for label in manifest.metadata.labels.iter() {
371        if !valid_oam_label(label) {
372            failures.push(ValidationFailure::new(
373                ValidationFailureLevel::Error,
374                format!("Invalid OAM label: {:?}", label),
375            ));
376        }
377    }
378
379    for annotation in manifest.metadata.annotations.iter() {
380        if !valid_oam_label(annotation) {
381            failures.push(ValidationFailure::new(
382                ValidationFailureLevel::Error,
383                format!("Invalid OAM annotation: {:?}", annotation),
384            ));
385        }
386    }
387
388    for component in manifest.spec.components.iter() {
389        // Component name validation : each component (components or providers) should have a unique name
390        if !name_registry.insert(component.name.clone()) {
391            failures.push(ValidationFailure::new(
392                ValidationFailureLevel::Error,
393                format!("Duplicate component name in manifest: {}", component.name),
394            ));
395        }
396        // Provider validation :
397        // Provider config should be serializable [For all components that have JSON config, validate that it can serialize.
398        // We need this so it doesn't trigger an error when sending a command down the line]
399        // Providers should have a unique image ref and link name
400        if let Properties::Capability {
401            properties:
402                CapabilityProperties {
403                    id: Some(component_id),
404                    config: _capability_config,
405                    ..
406                },
407        } = &component.properties
408        {
409            if !id_registry.insert(component_id.to_string()) {
410                failures.push(ValidationFailure::new(
411                    ValidationFailureLevel::Error,
412                    format!(
413                        "Duplicate component identifier in manifest: {}",
414                        component_id
415                    ),
416                ));
417            }
418        }
419
420        // Component validation : Components should have a unique identifier per manifest
421        if let Properties::Component {
422            properties: ComponentProperties { id: Some(id), .. },
423        } = &component.properties
424        {
425            if !id_registry.insert(id.to_string()) {
426                failures.push(ValidationFailure::new(
427                    ValidationFailureLevel::Error,
428                    format!("Duplicate component identifier in manifest: {}", id),
429                ));
430            }
431        }
432
433        // Linkdef validation : A linkdef from a component should have a unique target and reference
434        if let Some(traits_vec) = &component.traits {
435            for trait_item in traits_vec.iter() {
436                if let Trait {
437                    // TODO : add trait type validation after custom types are done. See TraitProperty enum.
438                    properties: TraitProperty::Link(LinkProperty { target, .. }),
439                    ..
440                } = &trait_item
441                {
442                    // Multiple components{ with type != 'capability'} can declare the same target, so we don't need to check for duplicates on insert
443                    required_capability_components.insert(target.name.to_string());
444                }
445            }
446        }
447    }
448
449    let missing_capability_components = required_capability_components
450        .difference(&name_registry)
451        .collect::<Vec<&String>>();
452
453    if !missing_capability_components.is_empty() {
454        failures.push(ValidationFailure::new(
455            ValidationFailureLevel::Error,
456            format!(
457                "The following capability component(s) are missing from the manifest: {:?}",
458                missing_capability_components
459            ),
460        ));
461    };
462    failures
463}
464
465/// Check for misnamed host-supported interfaces in the manifest
466fn check_misnamed_interfaces(manifest: &Manifest) -> Vec<ValidationFailure> {
467    let mut failures = Vec::new();
468    for link_trait in manifest.links() {
469        if let TraitProperty::Link(LinkProperty {
470            namespace,
471            package,
472            interfaces,
473            target: _target,
474            source: _source,
475            ..
476        }) = &link_trait.properties
477        {
478            for interface in interfaces {
479                failures.extend(is_invalid_known_interface(namespace, package, interface))
480            }
481        }
482    }
483
484    failures
485}
486
487/// This validation rule should eventually be removed, but at this time (as of wadm 0.14.0)
488/// custom traits are not supported. We technically deserialize the custom trait, but 99%
489/// of the time this is just a poorly formatted spread or link scaler which is incredibly
490/// frustrating to debug.
491fn ensure_no_custom_traits(manifest: &Manifest) -> Vec<ValidationFailure> {
492    let mut failures = Vec::new();
493    for component in manifest.components() {
494        if let Some(traits) = &component.traits {
495            for trait_item in traits {
496                match &trait_item.properties {
497                    TraitProperty::Custom(trt) if trait_item.is_link() => failures.push(ValidationFailure::new(
498                        ValidationFailureLevel::Error,
499                        format!("Link trait deserialized as custom trait, ensure fields are correct: {}", trt),
500                    )),
501                    TraitProperty::Custom(trt) if trait_item.is_scaler() => failures.push(ValidationFailure::new(
502                        ValidationFailureLevel::Error,
503                        format!("Scaler trait deserialized as custom trait, ensure fields are correct: {}", trt),
504                    )),
505                    _ => (),
506                }
507            }
508        }
509    }
510    failures
511}
512
513/// Check for "dangling" links, which contain targets that are not specified elsewhere in the
514/// WADM manifest.
515///
516/// A problem of this type only constitutes a warning, because it is possible that the manifest
517/// does not *completely* specify targets (they may be deployed/managed external to WADM or in a separte
518/// manifest).
519fn check_dangling_links(manifest: &Manifest) -> Vec<ValidationFailure> {
520    let lookup = manifest.component_lookup();
521    let mut failures = Vec::new();
522    for link_trait in manifest.links() {
523        match &link_trait.properties {
524            TraitProperty::Custom(obj) => {
525                if obj.get("target").is_none() {
526                    failures.push(ValidationFailure::new(
527                        ValidationFailureLevel::Error,
528                        "custom link is missing 'target' property".into(),
529                    ));
530                    continue;
531                }
532
533                // Ensure target property is present
534                match obj["target"]["name"].as_str() {
535                    // If target is present, ensure it's pointing to a known component
536                    Some(target) if !lookup.contains_key(&String::from(target)) => {
537                        failures.push(ValidationFailure::new(
538                            ValidationFailureLevel::Warning,
539                            format!("custom link target [{target}] is not a listed component"),
540                        ))
541                    }
542                    // For all keys where the the component is in the lookup we can do nothing
543                    Some(_) => {}
544                    // if target property is not present, note that it is missing
545                    None => failures.push(ValidationFailure::new(
546                        ValidationFailureLevel::Error,
547                        "custom link is missing 'target' name property".into(),
548                    )),
549                }
550            }
551            TraitProperty::Link(LinkProperty { name, target, .. }) => {
552                let link_identifier = name
553                    .as_ref()
554                    .map(|n| format!("(name [{n}])"))
555                    .unwrap_or_else(|| format!("(target [{}])", target.name));
556                if !lookup.contains_key(&target.name) {
557                    failures.push(ValidationFailure::new(
558                        ValidationFailureLevel::Warning,
559                        format!(
560                            "link {link_identifier} target [{}] is not a listed component",
561                            target.name
562                        ),
563                    ))
564                }
565            }
566
567            _ => unreachable!("manifest.links() should only return links"),
568        }
569    }
570
571    failures
572}
573
574/// Ensure that a manifest has secrets that are mapped to known policies
575/// and that those policies have the expected type and properties.
576fn validate_policies(manifest: &Manifest) -> Vec<ValidationFailure> {
577    let policies = manifest.policy_lookup();
578    let mut failures = Vec::new();
579    for c in manifest.components() {
580        // Ensure policies meant for secrets are valid
581        for secret in c.secrets() {
582            match policies.get(&secret.properties.policy) {
583                Some(policy) if policy.policy_type != SECRET_POLICY_TYPE => {
584                    failures.push(ValidationFailure::new(
585                        ValidationFailureLevel::Error,
586                        format!(
587                            "secret '{}' is mapped to policy '{}' which is not a secret policy. Expected type '{SECRET_POLICY_TYPE}'",
588                            secret.name, secret.properties.policy
589                        ),
590                    ))
591                }
592                Some(policy) => {
593                    if !policy.properties.contains_key("backend") {
594                        failures.push(ValidationFailure::new(
595                            ValidationFailureLevel::Error,
596                            format!(
597                                "secret '{}' is mapped to policy '{}' which does not include a 'backend' property",
598                                secret.name, secret.properties.policy
599                            ),
600                        ))
601                    }
602                }
603                None => failures.push(ValidationFailure::new(
604                    ValidationFailureLevel::Error,
605                    format!(
606                        "secret '{}' is mapped to unknown policy '{}'",
607                        secret.name, secret.properties.policy
608                    ),
609                )),
610            }
611        }
612    }
613    failures
614}
615
616/// Ensure that all components in a manifest either specify an image reference or a shared
617/// component in a different manifest. Note that this does not validate that the image reference
618/// is valid or that the shared component is valid, only that one of the two properties is set.
619pub fn validate_component_properties(application: &Manifest) -> Vec<ValidationFailure> {
620    let mut failures = Vec::new();
621    for component in application.spec.components.iter() {
622        match &component.properties {
623            Properties::Component {
624                properties:
625                    ComponentProperties {
626                        image,
627                        application,
628                        config,
629                        secrets,
630                        ..
631                    },
632            }
633            | Properties::Capability {
634                properties:
635                    CapabilityProperties {
636                        image,
637                        application,
638                        config,
639                        secrets,
640                        ..
641                    },
642            } => match (image, application) {
643                (Some(_), Some(_)) => {
644                    failures.push(ValidationFailure::new(
645                        ValidationFailureLevel::Error,
646                        "Component cannot have both 'image' and 'application' properties".into(),
647                    ));
648                }
649                (None, None) => {
650                    failures.push(ValidationFailure::new(
651                        ValidationFailureLevel::Error,
652                        "Component must have either 'image' or 'application' property".into(),
653                    ));
654                }
655                // This is a problem because of our left-folding config implementation. A shared application
656                // could specify additional config and actually overwrite the original manifest's config.
657                (None, Some(shared_properties)) if !config.is_empty() => {
658                    failures.push(ValidationFailure::new(
659                        ValidationFailureLevel::Error,
660                        format!(
661                            "Shared component '{}' cannot specify additional 'config'",
662                            shared_properties.name
663                        ),
664                    ));
665                }
666                (None, Some(shared_properties)) if !secrets.is_empty() => {
667                    failures.push(ValidationFailure::new(
668                        ValidationFailureLevel::Error,
669                        format!(
670                            "Shared component '{}' cannot specify additional 'secrets'",
671                            shared_properties.name
672                        ),
673                    ));
674                }
675                // Shared application components already have scale properties defined in their original manifest
676                (None, Some(shared_properties))
677                    if component
678                        .traits
679                        .as_ref()
680                        .is_some_and(|traits| traits.iter().any(|trt| trt.is_scaler())) =>
681                {
682                    failures.push(ValidationFailure::new(
683                        ValidationFailureLevel::Error,
684                        format!(
685                            "Shared component '{}' cannot include a scaler trait",
686                            shared_properties.name
687                        ),
688                    ));
689                }
690                _ => {}
691            },
692        }
693    }
694    failures
695}
696
697/// Validates link configs in a WADM application manifest.
698///
699/// At present this can check for:
700/// - all configs that declare `properties` have unique names
701///   (configs without properties refer to existing configs)
702///
703pub fn validate_link_configs(manifest: &Manifest) -> Vec<ValidationFailure> {
704    let mut failures = Vec::new();
705    let mut link_config_names = HashSet::new();
706    for link_trait in manifest.links() {
707        if let TraitProperty::Link(LinkProperty { target, source, .. }) = &link_trait.properties {
708            for config in &target.config {
709                // we only need to check for uniqueness of configs with properties
710                if config.properties.is_none() {
711                    continue;
712                }
713                // Check if config name is unique
714                if !link_config_names.insert(config.name.clone()) {
715                    failures.push(ValidationFailure::new(
716                        ValidationFailureLevel::Error,
717                        format!("Duplicate link config name found: '{}'", config.name),
718                    ));
719                }
720            }
721
722            if let Some(source) = source {
723                for config in &source.config {
724                    // we only need to check for uniqueness of configs with properties
725                    if config.properties.is_none() {
726                        continue;
727                    }
728                    // Check if config name is unique
729                    if !link_config_names.insert(config.name.clone()) {
730                        failures.push(ValidationFailure::new(
731                            ValidationFailureLevel::Error,
732                            format!("Duplicate link config name found: '{}'", config.name),
733                        ));
734                    }
735                }
736            }
737        }
738    }
739    failures
740}
741
742/// Funtion to validate the component configs
743/// from 0.13.0 source_config is deprecated and replaced with source:config:
744/// this function validates the raw yaml to check for deprecated source_config and target_config
745pub fn validate_components_configs(application: &serde_yaml::Value) -> Vec<ValidationFailure> {
746    let mut failures = Vec::new();
747
748    if let Some(specs) = application.get("spec") {
749        if let Some(components) = specs.get("components") {
750            if let Some(components_sequence) = components.as_sequence() {
751                for component in components_sequence.iter() {
752                    failures.extend(get_deprecated_configs(component));
753                }
754            }
755        }
756    }
757    failures
758}
759
760fn get_deprecated_configs(component: &serde_yaml::Value) -> Vec<ValidationFailure> {
761    let mut failures = vec![];
762    if let Some(traits) = component.get("traits") {
763        if let Some(traits_sequence) = traits.as_sequence() {
764            for trait_ in traits_sequence.iter() {
765                if let Some(trait_type) = trait_.get("type") {
766                    if trait_type.ne("link") {
767                        continue;
768                    }
769                }
770                if let Some(trait_properties) = trait_.get("properties") {
771                    if trait_properties.get("source_config").is_some() {
772                        failures.push(ValidationFailure {
773                            level: ValidationFailureLevel::Warning,
774                            msg: "one of the components' link trait contains a source_config key, please use source:config: rather".to_string(),
775                        });
776                    }
777                    if trait_properties.get("target_config").is_some() {
778                        failures.push(ValidationFailure {
779                            level: ValidationFailureLevel::Warning,
780                            msg: "one of the components' link trait contains a target_config key, please use target:config: rather".to_string(),
781                        });
782                    }
783                }
784            }
785        }
786    }
787    failures
788}
789
790/// This function validates that a key/value pair is a valid OAM label. It's using fairly
791/// basic validation rules to ensure that the manifest isn't doing anything horribly wrong. Keeping
792/// this function free of regex is intentional to keep this code functional but simple.
793///
794/// See <https://github.com/oam-dev/spec/blob/master/metadata.md#metadata> for details
795pub fn valid_oam_label(label: (&String, &String)) -> bool {
796    let (key, _) = label;
797    match key.split_once('/') {
798        Some((prefix, name)) => is_valid_dns_subdomain(prefix) && is_valid_label_name(name),
799        None => is_valid_label_name(key),
800    }
801}
802
803pub fn is_valid_dns_subdomain(s: &str) -> bool {
804    if s.is_empty() || s.len() > 253 {
805        return false;
806    }
807
808    s.split('.').all(|part| {
809        // Ensure each part is non-empty, <= 63 characters, starts with an alphabetic character,
810        // ends with an alphanumeric character, and contains only alphanumeric characters or hyphens
811        !part.is_empty()
812            && part.len() <= 63
813            && part.starts_with(|c: char| c.is_ascii_alphabetic())
814            && part.ends_with(|c: char| c.is_ascii_alphanumeric())
815            && part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
816    })
817}
818
819// Ensure each name is non-empty, <= 63 characters, starts with an alphanumeric character,
820// ends with an alphanumeric character, and contains only alphanumeric characters, hyphens,
821// underscores, or periods
822pub fn is_valid_label_name(name: &str) -> bool {
823    if name.is_empty() || name.len() > 63 {
824        return false;
825    }
826
827    name.starts_with(|c: char| c.is_ascii_alphanumeric())
828        && name.ends_with(|c: char| c.is_ascii_alphanumeric())
829        && name
830            .chars()
831            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
832}
833
834/// Checks whether a manifest contains "duplicate" links.
835///
836/// Multiple links from the same source with the same name, namespace, package and interface
837/// are considered duplicate links.
838fn check_duplicate_links(manifest: &Manifest) -> Vec<ValidationFailure> {
839    let mut failures = Vec::new();
840    for component in manifest.components() {
841        let mut link_ids = HashSet::new();
842        for link in component.links() {
843            if let TraitProperty::Link(LinkProperty {
844                name,
845                namespace,
846                package,
847                interfaces,
848                ..
849            }) = &link.properties
850            {
851                for interface in interfaces {
852                    if !link_ids.insert((
853                        name.clone()
854                            .unwrap_or_else(|| DEFAULT_LINK_NAME.to_string()),
855                        namespace,
856                        package,
857                        interface,
858                    )) {
859                        failures.push(ValidationFailure::new(
860                            ValidationFailureLevel::Error,
861                            format!(
862                                "Duplicate link found inside component '{}': {} ({}:{}/{})",
863                                component.name,
864                                name.clone()
865                                    .unwrap_or_else(|| DEFAULT_LINK_NAME.to_string()),
866                                namespace,
867                                package,
868                                interface
869                            ),
870                        ));
871                    };
872                }
873            }
874        }
875    }
876    failures
877}
878
879#[cfg(test)]
880mod tests {
881    use super::is_valid_manifest_name;
882
883    const VALID_MANIFEST_NAMES: [&str; 4] = [
884        "mymanifest",
885        "my-manifest",
886        "my_manifest",
887        "mymanifest-v2-v3-final",
888    ];
889
890    const INVALID_MANIFEST_NAMES: [&str; 2] = ["my.manifest", "my manifest"];
891
892    /// Ensure valid manifest names pass
893    #[test]
894    fn manifest_names_valid() {
895        // Acceptable manifest names
896        for valid in VALID_MANIFEST_NAMES {
897            assert!(is_valid_manifest_name(valid));
898        }
899    }
900
901    /// Ensure invalid manifest names fail
902    #[test]
903    fn manifest_names_invalid() {
904        for invalid in INVALID_MANIFEST_NAMES {
905            assert!(!is_valid_manifest_name(invalid))
906        }
907    }
908}