|
| 1 | +use anyhow::anyhow; |
1 | 2 | use async_nats::{jetstream::stream::Stream, Client, Message};
|
2 | 3 | use base64::{engine::general_purpose::STANDARD as B64decoder, Engine};
|
3 | 4 | use regex::Regex;
|
4 | 5 | use serde_json::json;
|
| 6 | +use std::collections::{HashMap, HashSet}; |
5 | 7 | use tokio::sync::OnceCell;
|
6 | 8 | use tracing::{debug, error, instrument, log::warn, trace};
|
7 | 9 |
|
8 | 10 | use crate::{
|
9 | 11 | model::{
|
10 |
| - internal::StoredManifest, CapabilityConfig, CapabilityProperties, Properties, |
11 |
| - LATEST_VERSION, |
| 12 | + internal::StoredManifest, ActorProperties, CapabilityProperties, LinkdefProperty, Manifest, |
| 13 | + Properties, Trait, TraitProperty, LATEST_VERSION, |
12 | 14 | },
|
13 | 15 | publisher::Publisher,
|
14 | 16 | server::StatusType,
|
@@ -57,31 +59,6 @@ impl<P: Publisher> Handler<P> {
|
57 | 59 | return;
|
58 | 60 | }
|
59 | 61 |
|
60 |
| - // For all components that have JSON config, validate that it can serialize. We need this so |
61 |
| - // it doesn't trigger an error when sending a command down the line |
62 |
| - for component in manifest.spec.components.iter() { |
63 |
| - if let Properties::Capability { |
64 |
| - properties: |
65 |
| - CapabilityProperties { |
66 |
| - config: Some(CapabilityConfig::Json(data)), |
67 |
| - .. |
68 |
| - }, |
69 |
| - } = &component.properties |
70 |
| - { |
71 |
| - if let Err(e) = serde_json::to_string(data) { |
72 |
| - self.send_error( |
73 |
| - msg.reply, |
74 |
| - format!( |
75 |
| - "Unable to serialize JSON config data for component {}: {e:?}", |
76 |
| - component.name, |
77 |
| - ), |
78 |
| - ) |
79 |
| - .await; |
80 |
| - return; |
81 |
| - } |
82 |
| - } |
83 |
| - } |
84 |
| - |
85 | 62 | let manifest_name = manifest.metadata.name.trim().to_string();
|
86 | 63 | if !MANIFEST_NAME_REGEX
|
87 | 64 | // SAFETY: We know this is valid Regex
|
@@ -112,6 +89,11 @@ impl<P: Publisher> Handler<P> {
|
112 | 89 | }
|
113 | 90 | };
|
114 | 91 |
|
| 92 | + if let Some(error_message) = validate_manifest(manifest.clone()).err() { |
| 93 | + self.send_error(msg.reply, error_message.to_string()).await; |
| 94 | + return; |
| 95 | + } |
| 96 | + |
115 | 97 | let mut resp = PutModelResponse {
|
116 | 98 | // If we successfully insert, the given manifest version will be the new current version
|
117 | 99 | current_version: manifest.version().to_owned(),
|
@@ -811,8 +793,162 @@ impl<P: Publisher> Handler<P> {
|
811 | 793 | }
|
812 | 794 | }
|
813 | 795 |
|
| 796 | +// Manifest validation |
| 797 | +pub(crate) fn validate_manifest(manifest: Manifest) -> anyhow::Result<()> { |
| 798 | + let mut component_details: HashSet<String> = HashSet::new(); |
| 799 | + |
| 800 | + // Map of link names to a vector of provider references with that link name |
| 801 | + let mut linkdef_map: HashMap<String, Vec<String>> = HashMap::new(); |
| 802 | + |
| 803 | + for component in manifest.spec.components.iter() { |
| 804 | + // Component name validation : each component (actors or providers) should have a unique name |
| 805 | + if !component_details.insert(component.name.clone()) { |
| 806 | + return Err(anyhow!( |
| 807 | + "Duplicate component name in manifest: {}", |
| 808 | + component.name |
| 809 | + )); |
| 810 | + } |
| 811 | + // Provider validation : |
| 812 | + // Provider config should be serializable [For all components that have JSON config, validate that it can serialize. |
| 813 | + // We need this so it doesn't trigger an error when sending a command down the line] |
| 814 | + // Providers should have a unique image ref and link name |
| 815 | + if let Properties::Capability { |
| 816 | + properties: |
| 817 | + CapabilityProperties { |
| 818 | + image: image_name, |
| 819 | + link_name: Some(link), |
| 820 | + config: capability_config, |
| 821 | + .. |
| 822 | + }, |
| 823 | + } = &component.properties |
| 824 | + { |
| 825 | + if let Some(data) = capability_config { |
| 826 | + if let Err(e) = serde_json::to_string(data) { |
| 827 | + return Err(anyhow!( |
| 828 | + "Unable to serialize JSON config data for component {}: {e:?}", |
| 829 | + component.name |
| 830 | + )); |
| 831 | + } |
| 832 | + } |
| 833 | + |
| 834 | + if let Some(duplicate_ref) = linkdef_map.get_mut(link) { |
| 835 | + if duplicate_ref.contains(image_name) { |
| 836 | + return Err(anyhow!( |
| 837 | + "Duplicate image reference {} to link name {} in manifest", |
| 838 | + image_name, |
| 839 | + link |
| 840 | + )); |
| 841 | + } else { |
| 842 | + duplicate_ref.push(image_name.to_string()); |
| 843 | + } |
| 844 | + } |
| 845 | + linkdef_map.insert(link.to_string(), vec![image_name.to_string()]); |
| 846 | + } |
| 847 | + |
| 848 | + // Actor validation : Actors should have a unique name and reference |
| 849 | + if let Properties::Actor { |
| 850 | + properties: ActorProperties { image: image_name }, |
| 851 | + } = &component.properties |
| 852 | + { |
| 853 | + if !component_details.insert(image_name.to_string()) { |
| 854 | + return Err(anyhow!( |
| 855 | + "Duplicate image reference in manifest: {}", |
| 856 | + image_name |
| 857 | + )); |
| 858 | + } |
| 859 | + } |
| 860 | + |
| 861 | + // Linkdef validation : A linkdef from a component should have a unique target and reference |
| 862 | + let mut linkdef_set: HashSet<String> = HashSet::new(); |
| 863 | + if let Some(traits_vec) = &component.traits { |
| 864 | + for trait_item in traits_vec.iter() { |
| 865 | + if let Trait { |
| 866 | + // TODO : add trait type validation after custom types are done. See TraitProperty enum. |
| 867 | + properties: |
| 868 | + TraitProperty::Linkdef(LinkdefProperty { |
| 869 | + target: target_name, |
| 870 | + .. |
| 871 | + }), |
| 872 | + .. |
| 873 | + } = &trait_item |
| 874 | + { |
| 875 | + if !linkdef_set.insert(target_name.to_string()) { |
| 876 | + return Err(anyhow!( |
| 877 | + "Duplicate target for linkdef in manifest: {}", |
| 878 | + target_name |
| 879 | + )); |
| 880 | + } |
| 881 | + } |
| 882 | + } |
| 883 | + } |
| 884 | + } |
| 885 | + Ok(()) |
| 886 | +} |
| 887 | + |
814 | 888 | #[cfg(test)]
|
815 | 889 | mod test {
|
| 890 | + use std::io::BufReader; |
| 891 | + use std::path::Path; |
| 892 | + |
| 893 | + use super::*; |
| 894 | + use anyhow::Result; |
| 895 | + use serde_yaml; |
| 896 | + |
| 897 | + pub(crate) fn deserialize_yaml(filepath: impl AsRef<Path>) -> Result<Manifest> { |
| 898 | + let file = std::fs::File::open(filepath)?; |
| 899 | + let reader = BufReader::new(file); |
| 900 | + let yaml_string: Manifest = serde_yaml::from_reader(reader)?; |
| 901 | + Ok(yaml_string) |
| 902 | + } |
| 903 | + |
| 904 | + #[test] |
| 905 | + fn test_manifest_validation() { |
| 906 | + let correct_manifest = |
| 907 | + deserialize_yaml("./oam/simple1.yaml").expect("Should be able to parse"); |
| 908 | + |
| 909 | + assert!(validate_manifest(correct_manifest).is_ok()); |
| 910 | + |
| 911 | + let manifest = deserialize_yaml("./test/data/duplicate_component.yaml") |
| 912 | + .expect("Should be able to parse"); |
| 913 | + |
| 914 | + match validate_manifest(manifest) { |
| 915 | + Ok(()) => panic!("Should have detected duplicate component"), |
| 916 | + Err(e) => assert!(e |
| 917 | + .to_string() |
| 918 | + .contains("Duplicate component name in manifest")), |
| 919 | + } |
| 920 | + |
| 921 | + let manifest = deserialize_yaml("./test/data/duplicate_imageref1.yaml") |
| 922 | + .expect("Should be able to parse"); |
| 923 | + |
| 924 | + match validate_manifest(manifest) { |
| 925 | + Ok(()) => panic!("Should have detected duplicate image reference for link name in provider properties"), |
| 926 | + Err(e) => assert!(e |
| 927 | + .to_string() |
| 928 | + .contains("Duplicate image reference")), |
| 929 | + } |
| 930 | + |
| 931 | + let manifest = deserialize_yaml("./test/data/duplicate_imageref2.yaml") |
| 932 | + .expect("Should be able to parse"); |
| 933 | + |
| 934 | + match validate_manifest(manifest) { |
| 935 | + Ok(()) => panic!("Should have detected duplicate image reference for actor"), |
| 936 | + Err(e) => assert!(e |
| 937 | + .to_string() |
| 938 | + .contains("Duplicate image reference in manifest")), |
| 939 | + } |
| 940 | + |
| 941 | + let manifest = deserialize_yaml("./test/data/duplicate_linkdef.yaml") |
| 942 | + .expect("Should be able to parse"); |
| 943 | + |
| 944 | + match validate_manifest(manifest) { |
| 945 | + Ok(()) => panic!("Should have detected duplicate linkdef"), |
| 946 | + Err(e) => assert!(e |
| 947 | + .to_string() |
| 948 | + .contains("Duplicate target for linkdef in manifest")), |
| 949 | + } |
| 950 | + } |
| 951 | + |
816 | 952 | #[tokio::test]
|
817 | 953 | async fn manifest_name_regex_works() {
|
818 | 954 | let regex = super::MANIFEST_NAME_REGEX
|
|
0 commit comments