|
| 1 | +use std::{collections::HashMap, path::Path}; |
| 2 | + |
| 3 | +use anyhow::Context; |
| 4 | +use spin_common::ui::quoted_path; |
| 5 | +use spin_manifest::schema::v2::TargetEnvironmentRef; |
| 6 | + |
| 7 | +mod definition; |
| 8 | +mod env_loader; |
| 9 | +mod lockfile; |
| 10 | + |
| 11 | +use definition::WorldName; |
| 12 | + |
| 13 | +/// A fully realised deployment environment, e.g. Spin 2.7, |
| 14 | +/// SpinKube 3.1, Fermyon Cloud. The `TargetEnvironment` provides a mapping |
| 15 | +/// from the Spin trigger types supported in the environment to the Component Model worlds |
| 16 | +/// supported by that trigger type. (A trigger type may support more than one world, |
| 17 | +/// for example when it supports multiple versions of the Spin or WASI interfaces.) |
| 18 | +pub struct TargetEnvironment { |
| 19 | + name: String, |
| 20 | + trigger_worlds: HashMap<TriggerType, CandidateWorlds>, |
| 21 | + unknown_trigger: UnknownTrigger, |
| 22 | +} |
| 23 | + |
| 24 | +impl TargetEnvironment { |
| 25 | + /// Loads the specified list of environments. This fetches all required |
| 26 | + /// environment definitions from their references, and then chases packages |
| 27 | + /// references until the entire target environment is fully loaded. |
| 28 | + /// The function also caches registry references in the application directory, |
| 29 | + /// to avoid loading from the network when the app is validated again. |
| 30 | + pub async fn load_all( |
| 31 | + env_ids: &[TargetEnvironmentRef], |
| 32 | + cache_root: Option<std::path::PathBuf>, |
| 33 | + app_dir: &std::path::Path, |
| 34 | + ) -> anyhow::Result<Vec<Self>> { |
| 35 | + env_loader::load_environments(env_ids, cache_root, app_dir).await |
| 36 | + } |
| 37 | + |
| 38 | + /// The environment name for UI purposes |
| 39 | + pub fn name(&self) -> &str { |
| 40 | + &self.name |
| 41 | + } |
| 42 | + |
| 43 | + /// Returns true if the given trigger type can run in this environment. |
| 44 | + pub fn supports_trigger_type(&self, trigger_type: &TriggerType) -> bool { |
| 45 | + self.unknown_trigger.allows(trigger_type) || self.trigger_worlds.contains_key(trigger_type) |
| 46 | + } |
| 47 | + |
| 48 | + /// Lists all worlds supported for the given trigger type in this environment. |
| 49 | + pub fn worlds(&self, trigger_type: &TriggerType) -> &CandidateWorlds { |
| 50 | + self.trigger_worlds |
| 51 | + .get(trigger_type) |
| 52 | + .or_else(|| self.unknown_trigger.worlds()) |
| 53 | + .unwrap_or(NO_WORLDS) |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +/// How a `TargetEnvironment` should validate components associated with trigger types |
| 58 | +/// not listed in the/ environment definition. This is used for best-effort validation in |
| 59 | +/// extensible environments. |
| 60 | +/// |
| 61 | +/// For example, a "forgiving" definition of Spin CLI environment would |
| 62 | +/// validate that components associated with `cron` or `sqs` triggers adhere |
| 63 | +/// to the platform world, even though it cannot validate that the exports are correct |
| 64 | +/// or that the plugins are installed or up to date. This can result in failure at |
| 65 | +/// runtime, but that may be better than refusing to let cron jobs run! |
| 66 | +/// |
| 67 | +/// On the other hand, the SpinKube environment rejects unknown triggers |
| 68 | +/// because SpinKube does not allow arbitrary triggers to be linked at |
| 69 | +/// runtime: the set of triggers is static for a given version. |
| 70 | +enum UnknownTrigger { |
| 71 | + /// Components for unknown trigger types fail validation. |
| 72 | + Deny, |
| 73 | + /// Components for unknown trigger types pass validation if they |
| 74 | + /// conform to (at least) one of the listed worlds. |
| 75 | + Allow(CandidateWorlds), |
| 76 | +} |
| 77 | + |
| 78 | +impl UnknownTrigger { |
| 79 | + fn allows(&self, _trigger_type: &TriggerType) -> bool { |
| 80 | + matches!(self, Self::Allow(_)) |
| 81 | + } |
| 82 | + |
| 83 | + fn worlds(&self) -> Option<&CandidateWorlds> { |
| 84 | + match self { |
| 85 | + Self::Deny => None, |
| 86 | + Self::Allow(cw) => Some(cw), |
| 87 | + } |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +/// The set of worlds that a particular trigger type (in a given environment) |
| 92 | +/// can accept. For example, the Spin 3.2 CLI `http` trigger accepts various |
| 93 | +/// versions of the `spin:up/http-trigger` world. |
| 94 | +/// |
| 95 | +/// A component will pass target validation if it conforms to |
| 96 | +/// at least one of these worlds. |
| 97 | +#[derive(Default)] |
| 98 | +pub struct CandidateWorlds { |
| 99 | + worlds: Vec<CandidateWorld>, |
| 100 | +} |
| 101 | + |
| 102 | +impl<'a> IntoIterator for &'a CandidateWorlds { |
| 103 | + type Item = &'a CandidateWorld; |
| 104 | + |
| 105 | + type IntoIter = std::slice::Iter<'a, CandidateWorld>; |
| 106 | + |
| 107 | + fn into_iter(self) -> Self::IntoIter { |
| 108 | + self.worlds.iter() |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +const NO_WORLDS: &CandidateWorlds = &CandidateWorlds { worlds: vec![] }; |
| 113 | + |
| 114 | +/// A WIT world; specifically, a WIT world provided by a Spin host, against which |
| 115 | +/// a component can be validated. |
| 116 | +pub struct CandidateWorld { |
| 117 | + world: WorldName, |
| 118 | + package: wit_parser::Package, |
| 119 | + package_bytes: Vec<u8>, |
| 120 | +} |
| 121 | + |
| 122 | +impl std::fmt::Display for CandidateWorld { |
| 123 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 124 | + self.world.fmt(f) |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +impl CandidateWorld { |
| 129 | + /// Namespaced but unversioned package name (e.g. spin:up) |
| 130 | + pub fn package_namespaced_name(&self) -> String { |
| 131 | + format!("{}:{}", self.package.name.namespace, self.package.name.name) |
| 132 | + } |
| 133 | + |
| 134 | + /// The package version for the environment package. |
| 135 | + pub fn package_version(&self) -> Option<&semver::Version> { |
| 136 | + self.package.name.version.as_ref() |
| 137 | + } |
| 138 | + |
| 139 | + /// The Wasm-encoded bytes of the environment package. |
| 140 | + pub fn package_bytes(&self) -> &[u8] { |
| 141 | + &self.package_bytes |
| 142 | + } |
| 143 | + |
| 144 | + fn from_package_bytes(world: &WorldName, bytes: Vec<u8>) -> anyhow::Result<Self> { |
| 145 | + let decoded = wit_component::decode(&bytes) |
| 146 | + .with_context(|| format!("Failed to decode package for environment {world}"))?; |
| 147 | + let package_id = decoded.package(); |
| 148 | + let package = decoded |
| 149 | + .resolve() |
| 150 | + .packages |
| 151 | + .get(package_id) |
| 152 | + .with_context(|| { |
| 153 | + format!("The {world} package is invalid (no package for decoded package ID)") |
| 154 | + })? |
| 155 | + .clone(); |
| 156 | + |
| 157 | + Ok(Self { |
| 158 | + world: world.to_owned(), |
| 159 | + package, |
| 160 | + package_bytes: bytes, |
| 161 | + }) |
| 162 | + } |
| 163 | + |
| 164 | + fn from_decoded_wasm( |
| 165 | + world: &WorldName, |
| 166 | + source: &Path, |
| 167 | + decoded: wit_parser::decoding::DecodedWasm, |
| 168 | + ) -> anyhow::Result<Self> { |
| 169 | + let package_id = decoded.package(); |
| 170 | + let package = decoded |
| 171 | + .resolve() |
| 172 | + .packages |
| 173 | + .get(package_id) |
| 174 | + .with_context(|| { |
| 175 | + format!( |
| 176 | + "The {} environment is invalid (no package for decoded package ID)", |
| 177 | + quoted_path(source) |
| 178 | + ) |
| 179 | + })? |
| 180 | + .clone(); |
| 181 | + |
| 182 | + let bytes = wit_component::encode(decoded.resolve(), package_id)?; |
| 183 | + |
| 184 | + Ok(Self { |
| 185 | + world: world.to_owned(), |
| 186 | + package, |
| 187 | + package_bytes: bytes, |
| 188 | + }) |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +pub type TriggerType = String; |
0 commit comments