From 2a59e27b36f09d1b5b838c6ad237fd662da55f14 Mon Sep 17 00:00:00 2001
From: Javier Viola <363911+pepoviola@users.noreply.github.com>
Date: Sun, 3 Mar 2024 12:24:39 -0300
Subject: [PATCH] Validate spec for provider (#179)

Add small validation, mostly to ensure that the `image` is set in k8s.

fix #143
---
 crates/configuration/src/parachain.rs      |   2 +-
 crates/orchestrator/src/errors.rs          |   2 +
 crates/orchestrator/src/lib.rs             | 117 ++++++++++++++++++++-
 crates/provider/src/kubernetes/provider.rs |   6 ++
 crates/provider/src/lib.rs                 |   2 +
 crates/provider/src/native/provider.rs     |   6 ++
 6 files changed, 129 insertions(+), 6 deletions(-)

diff --git a/crates/configuration/src/parachain.rs b/crates/configuration/src/parachain.rs
index 8d07b7f..4e027e6 100644
--- a/crates/configuration/src/parachain.rs
+++ b/crates/configuration/src/parachain.rs
@@ -687,7 +687,7 @@ impl<C: Context> ParachainConfigBuilder<WithId, C> {
     /// Add a new collator using a nested [`NodeConfigBuilder`].
     pub fn with_collator(
         self,
-        f: fn(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
+        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
     ) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
         match f(NodeConfigBuilder::new(
             self.default_chain_context(),
diff --git a/crates/orchestrator/src/errors.rs b/crates/orchestrator/src/errors.rs
index d8cd058..8a65c09 100644
--- a/crates/orchestrator/src/errors.rs
+++ b/crates/orchestrator/src/errors.rs
@@ -10,6 +10,8 @@ pub enum OrchestratorError {
     // TODO: improve invalid config reporting
     #[error("Invalid network configuration: {0}")]
     InvalidConfig(String),
+    #[error("Invalid network config to use provider {0}: {1}")]
+    InvalidConfigForProvider(String, String),
     #[error("Invalid configuration for node: {0}, field: {1}")]
     InvalidNodeConfig(String, String),
     #[error("Invariant not fulfilled {0}")]
diff --git a/crates/orchestrator/src/lib.rs b/crates/orchestrator/src/lib.rs
index a4d8dc0..9da760f 100644
--- a/crates/orchestrator/src/lib.rs
+++ b/crates/orchestrator/src/lib.rs
@@ -18,7 +18,10 @@ use configuration::{NetworkConfig, RegistrationStrategy};
 use errors::OrchestratorError;
 use network::{parachain::Parachain, relaychain::Relaychain, Network};
 use network_spec::{parachain::ParachainSpec, NetworkSpec};
-use provider::{types::TransferedFile, DynProvider};
+use provider::{
+    types::{ProviderCapabilities, TransferedFile},
+    DynProvider,
+};
 use support::fs::{FileSystem, FileSystemError};
 use tokio::time::timeout;
 use tracing::{debug, info};
@@ -70,16 +73,21 @@ where
         // main driver for spawn the network
         debug!("Network spec to spawn, {:#?}", network_spec);
 
+        // TODO: move to Provider trait
+        validate_spec_with_provider_capabilities(&network_spec, self.provider.capabilities())
+            .map_err(|err| {
+                OrchestratorError::InvalidConfigForProvider(
+                    self.provider.name().into(),
+                    err.to_string(),
+                )
+            })?;
+
         // create namespace
         let ns = self.provider.create_namespace().await?;
 
         info!("🧰 ns: {}", ns.name());
         info!("🧰 base_dir: {:?}", ns.base_dir());
 
-        // TODO: noop for native
-        // Static setup
-        // ns.static_setup().await?;
-
         let base_dir = ns.base_dir().to_string_lossy();
         let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
         // Create chain-spec for relaychain
@@ -357,6 +365,41 @@ where
     }
 }
 
+// Validate that the config fulfill all the requirements of the provider
+fn validate_spec_with_provider_capabilities(
+    network_spec: &NetworkSpec,
+    capabilities: &ProviderCapabilities,
+) -> Result<(), anyhow::Error> {
+    if !capabilities.requires_image {
+        return Ok(());
+    }
+
+    // Relaychain
+    if network_spec.relaychain.default_image.is_none() {
+        // we should check if each node have an image
+        let nodes = &network_spec.relaychain.nodes;
+        if nodes.iter().any(|node| node.image.is_none()) {
+            return Err(anyhow::anyhow!(
+                "missing image for node, and not default is set at relaychain"
+            ));
+        }
+    };
+
+    // Paras
+    for para in &network_spec.parachains {
+        if para.default_image.is_none() {
+            let nodes = &para.collators;
+            if nodes.iter().any(|node| node.image.is_none()) {
+                return Err(anyhow::anyhow!(
+                    "missing image for node, and not default is set at parachain {}",
+                    para.id
+                ));
+            }
+        }
+    }
+
+    Ok(())
+}
 // TODO: get the fs from `DynNamespace` will make this not needed
 // but the FileSystem trait isn't object-safe so we can't pass around
 // as `dyn FileSystem`. We can refactor or using some `erase` techniques
@@ -444,3 +487,67 @@ pub enum ZombieRole {
 // re-export
 pub use network::{AddCollatorOptions, AddNodeOptions};
 pub use shared::types::PjsResult;
+
+#[cfg(test)]
+mod tests {
+    use configuration::NetworkConfigBuilder;
+
+    use super::*;
+
+    fn generate(with_image: bool) -> Result<NetworkConfig, Vec<anyhow::Error>> {
+        NetworkConfigBuilder::new()
+            .with_relaychain(|r| {
+                let mut relay = r
+                    .with_chain("rococo-local")
+                    .with_default_command("polkadot");
+                if with_image {
+                    relay = relay.with_default_image("docker.io/parity/polkadot")
+                }
+
+                relay
+                    .with_node(|node| node.with_name("alice"))
+                    .with_node(|node| node.with_name("bob"))
+            })
+            .with_parachain(|p| {
+                p.with_id(2000).cumulus_based(true).with_collator(|n| {
+                    let node = n.with_name("collator").with_command("polkadot-parachain");
+                    if with_image {
+                        node.with_image("docker.io/paritypr/test-parachain")
+                    } else {
+                        node
+                    }
+                })
+            })
+            .build()
+    }
+
+    #[tokio::test]
+    async fn valid_config_with_image() {
+        let network_config = generate(true).unwrap();
+        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
+        let caps = ProviderCapabilities {
+            requires_image: true,
+            has_resources: false,
+            prefix_with_full_path: false,
+            use_default_ports_in_cmd: false,
+        };
+
+        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
+        assert!(valid.is_ok())
+    }
+
+    #[tokio::test]
+    async fn invalid_config() {
+        let network_config = generate(false).unwrap();
+        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
+        let caps = ProviderCapabilities {
+            requires_image: true,
+            has_resources: false,
+            prefix_with_full_path: false,
+            use_default_ports_in_cmd: false,
+        };
+
+        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
+        assert!(valid.is_err())
+    }
+}
diff --git a/crates/provider/src/kubernetes/provider.rs b/crates/provider/src/kubernetes/provider.rs
index fa73a92..29cba80 100644
--- a/crates/provider/src/kubernetes/provider.rs
+++ b/crates/provider/src/kubernetes/provider.rs
@@ -13,6 +13,8 @@ use crate::{
     types::ProviderCapabilities, DynNamespace, Provider, ProviderError, ProviderNamespace,
 };
 
+const PROVIDER_NAME: &str = "k8s";
+
 pub struct KubernetesProvider<FS>
 where
     FS: FileSystem + Send + Sync + Clone,
@@ -58,6 +60,10 @@ impl<FS> Provider for KubernetesProvider<FS>
 where
     FS: FileSystem + Send + Sync + Clone + 'static,
 {
+    fn name(&self) -> &str {
+        PROVIDER_NAME
+    }
+
     fn capabilities(&self) -> &ProviderCapabilities {
         &self.capabilities
     }
diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs
index 5e6ccf9..f7139c3 100644
--- a/crates/provider/src/lib.rs
+++ b/crates/provider/src/lib.rs
@@ -104,6 +104,8 @@ pub enum ProviderError {
 
 #[async_trait]
 pub trait Provider {
+    fn name(&self) -> &str;
+
     fn capabilities(&self) -> &ProviderCapabilities;
 
     async fn namespaces(&self) -> HashMap<String, DynNamespace>;
diff --git a/crates/provider/src/native/provider.rs b/crates/provider/src/native/provider.rs
index 5124c70..799c379 100644
--- a/crates/provider/src/native/provider.rs
+++ b/crates/provider/src/native/provider.rs
@@ -13,6 +13,8 @@ use crate::{
     types::ProviderCapabilities, DynNamespace, Provider, ProviderError, ProviderNamespace,
 };
 
+const PROVIDER_NAME: &str = "native";
+
 pub struct NativeProvider<FS>
 where
     FS: FileSystem + Send + Sync + Clone,
@@ -58,6 +60,10 @@ impl<FS> Provider for NativeProvider<FS>
 where
     FS: FileSystem + Send + Sync + Clone + 'static,
 {
+    fn name(&self) -> &str {
+        PROVIDER_NAME
+    }
+
     fn capabilities(&self) -> &ProviderCapabilities {
         &self.capabilities
     }
-- 
GitLab