wick_config/config/
component_config.rs

1#![allow(missing_docs)] // delete when we move away from the `property` crate.
2mod composite;
3mod wasm_component_model;
4mod wasmrs;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use asset_container::{AssetManager, Assets};
9pub use composite::*;
10use config::{ComponentImplementation, ComponentKind};
11use tracing::trace;
12pub use wasm_component_model::{
13  WasmComponentDefinition,
14  WasmComponentDefinitionBuilder,
15  WasmComponentDefinitionBuilderError,
16};
17pub use wasmrs::{WasmRsComponent, WasmRsComponentBuilder, WasmRsComponentBuilderError};
18use wick_asset_reference::{AssetReference, FetchOptions};
19use wick_interface_types::{ComponentMetadata, ComponentSignature, Field, OperationSignature, TypeDefinition};
20use wick_packet::{Entity, RuntimeConfig};
21
22use super::common::package_definition::PackageConfig;
23use super::import_cache::{setup_cache, ImportCache};
24use super::{Binding, ImportDefinition, InterfaceDefinition, ResourceDefinition, TestConfiguration};
25use crate::config::template_config::Renderable;
26use crate::lockdown::{validate_resource, FailureKind, Lockdown, LockdownError};
27use crate::utils::{make_resolver, RwOption};
28use crate::{config, Error, Resolver, Result};
29
30#[derive(
31  Debug,
32  Default,
33  Clone,
34  derive_builder::Builder,
35  derive_asset_container::AssetManager,
36  property::Property,
37  serde::Serialize,
38)]
39#[builder(
40  derive(Debug),
41  setter(into),
42  build_fn(name = "build_internal", private, error = "crate::error::BuilderError")
43)]
44#[property(get(public), set(public), mut(public, suffix = "_mut"))]
45#[asset(asset(AssetReference))]
46#[must_use]
47/// A Wick component configuration.
48///
49/// A component configuration defines a wick component and its operations along with its dependencies
50/// immediate dependencies and any dependencies that it requires be provided by the user.
51pub struct ComponentConfiguration {
52  #[asset(skip)]
53  #[builder(setter(strip_option), default)]
54  #[serde(skip_serializing_if = "Option::is_none")]
55  /// The name of the component configuration.
56  pub(crate) name: Option<String>,
57
58  /// The component implementation.
59  pub(crate) component: ComponentImplementation,
60
61  #[asset(skip)]
62  #[builder(setter(strip_option), default)]
63  #[property(skip)]
64  #[serde(skip_serializing_if = "Option::is_none")]
65  /// The source (i.e. url or file on disk) of the configuration.
66  pub(crate) source: Option<PathBuf>,
67
68  #[asset(skip)]
69  #[builder(default)]
70  #[property(skip)]
71  /// Any types referenced or exported by this component.
72  #[serde(skip_serializing_if = "Vec::is_empty")]
73  pub(crate) types: Vec<TypeDefinition>,
74
75  #[builder(default)]
76  /// Any imports this component makes available to its implementation.
77  #[serde(skip_serializing_if = "Vec::is_empty")]
78  pub(crate) import: Vec<Binding<ImportDefinition>>,
79
80  #[asset(skip)]
81  #[builder(default)]
82  /// Any components or resources that must be provided to this component upon instantiation.
83  #[serde(skip_serializing_if = "Vec::is_empty")]
84  pub(crate) requires: Vec<Binding<InterfaceDefinition>>,
85
86  #[builder(default)]
87  /// Any resources this component defines.
88  #[serde(skip_serializing_if = "Vec::is_empty")]
89  pub(crate) resources: Vec<Binding<ResourceDefinition>>,
90
91  #[asset(skip)]
92  #[builder(default)]
93  /// The configuration to use when running this component as a microservice.
94  #[serde(skip_serializing_if = "Option::is_none")]
95  pub(crate) host: Option<config::HostConfig>,
96
97  #[asset(skip)]
98  #[builder(default)]
99  /// Any embedded test cases that should be run against this component.
100  #[serde(skip_serializing_if = "Vec::is_empty")]
101  pub(crate) tests: Vec<TestConfiguration>,
102
103  #[asset(skip)]
104  #[builder(default)]
105  /// The metadata for this component.
106  #[serde(skip_serializing_if = "Option::is_none")]
107  pub(crate) metadata: Option<config::Metadata>,
108
109  #[builder(default)]
110  /// The package configuration for this component.
111  #[serde(skip_serializing_if = "Option::is_none")]
112  pub(crate) package: Option<PackageConfig>,
113
114  #[asset(skip)]
115  #[doc(hidden)]
116  #[builder(default)]
117  #[serde(skip_serializing_if = "Option::is_none")]
118  pub(crate) root_config: Option<RuntimeConfig>,
119
120  #[asset(skip)]
121  #[builder(setter(skip))]
122  #[property(skip)]
123  #[doc(hidden)]
124  #[serde(skip)]
125  pub(crate) type_cache: ImportCache,
126
127  #[asset(skip)]
128  #[builder(setter(skip))]
129  #[property(skip)]
130  #[doc(hidden)]
131  #[serde(skip)]
132  pub(crate) cached_types: RwOption<Vec<TypeDefinition>>,
133}
134
135impl ComponentConfiguration {
136  /// Unwrap the inner composite component implementation or return an error.
137  pub const fn try_composite(&self) -> Result<&CompositeComponentImplementation> {
138    match &self.component {
139      ComponentImplementation::Composite(c) => Ok(c),
140      _ => Err(Error::UnexpectedComponentType(
141        ComponentKind::Composite,
142        self.component.kind(),
143      )),
144    }
145  }
146
147  /// Unwrap the inner wasm component implementation or return an error.
148  pub const fn try_wasmrs(&self) -> Result<&WasmRsComponent> {
149    match &self.component {
150      ComponentImplementation::WasmRs(c) => Ok(c),
151      _ => Err(Error::UnexpectedComponentType(
152        ComponentKind::WasmRs,
153        self.component.kind(),
154      )),
155    }
156  }
157
158  /// Get the package files
159  #[must_use]
160  pub fn package_files(&self) -> Option<Assets<AssetReference>> {
161    // should return empty vec if package is None
162    self.package.as_ref().map(|p| p.assets())
163  }
164
165  /// Set the source location of the configuration.
166  pub fn set_source(&mut self, source: &Path) {
167    let source = source.to_path_buf();
168    self.source = Some(source);
169  }
170
171  pub(super) fn update_baseurls(&self) {
172    #[allow(clippy::expect_used)]
173    let mut source = self.source.clone().expect("No source set for this configuration");
174    // Source is (should be) a file, so pop the filename before setting the baseurl.
175    if !source.is_dir() {
176      source.pop();
177    }
178
179    self.set_baseurl(&source);
180  }
181
182  /// Returns a function that resolves a binding to a configuration item.
183  #[must_use]
184  pub fn resolver(&self) -> Box<Resolver> {
185    make_resolver(self.import.clone(), self.resources.clone())
186  }
187
188  /// Get the kind of this component implementation.
189  pub const fn kind(&self) -> ComponentKind {
190    self.component.kind()
191  }
192
193  /// Determine if the configuration allows for fetching artifacts with the :latest tag.
194  #[must_use]
195  pub fn allow_latest(&self) -> bool {
196    self.host.as_ref().map_or(false, |v| v.allow_latest)
197  }
198
199  /// Return the list of insecure registries defined in the manifest
200  #[must_use]
201  pub fn insecure_registries(&self) -> Option<&[String]> {
202    self.host.as_ref().map(|v| v.insecure_registries.as_ref())
203  }
204
205  /// Return the version of the component.
206  #[must_use]
207  pub fn version(&self) -> Option<&str> {
208    self.metadata.as_ref().map(|m| m.version.as_str())
209  }
210
211  /// Return the underlying version of the source manifest.
212  #[must_use]
213  pub fn source(&self) -> Option<&Path> {
214    self.source.as_deref()
215  }
216
217  /// Return the types defined in this component.
218  pub fn types(&self) -> Result<Vec<TypeDefinition>> {
219    self.cached_types.read().as_ref().map_or_else(
220      || {
221        if self.import.is_empty() {
222          Ok(self.types.clone())
223        } else {
224          Err(Error::TypesNotFetched)
225        }
226      },
227      |types| Ok(types.clone()),
228    )
229  }
230
231  /// Get a mutable reference to the type definitions for this component.
232  pub fn types_mut(&mut self) -> &mut Vec<TypeDefinition> {
233    &mut self.types
234  }
235
236  /// Fetch/cache anything critical to the first use of this configuration.
237  pub(crate) async fn setup_cache(&self, options: FetchOptions) -> Result<()> {
238    setup_cache(
239      &self.type_cache,
240      self.import.iter(),
241      &self.cached_types,
242      self.types.clone(),
243      options,
244    )
245    .await
246  }
247
248  #[must_use]
249  pub fn config(&self) -> &[Field] {
250    match &self.component {
251      ComponentImplementation::Composite(c) => &c.config,
252      ComponentImplementation::Wasm(c) => &c.config,
253      ComponentImplementation::WasmRs(c) => &c.config,
254      ComponentImplementation::Sql(c) => &c.config,
255      ComponentImplementation::HttpClient(c) => &c.config,
256    }
257  }
258
259  /// Get the component signature for this configuration.
260  pub fn signature(&self) -> Result<ComponentSignature> {
261    let mut sig = wick_interface_types::component! {
262      name: self.name().cloned().unwrap_or_else(||self.component.default_name().to_owned()),
263      version: self.version(),
264      operations: self.component.operation_signatures(),
265    };
266    sig.config = self.config().to_vec();
267    sig.types = self.types()?;
268    Ok(sig)
269  }
270
271  /// Return the V1 yaml representation of this configuration.
272  #[cfg(feature = "v1")]
273  pub fn into_v1_yaml(self) -> Result<String> {
274    let v1_manifest: crate::v1::ComponentConfiguration = self.try_into()?;
275    Ok(serde_yaml::to_string(&v1_manifest).unwrap())
276  }
277
278  /// Initialize the configuration.
279  pub fn initialize(&mut self) -> Result<&Self> {
280    let root_config = self.root_config.as_ref();
281    let source = self.source().map(std::path::Path::to_path_buf);
282    trace!(
283      source = ?source,
284      num_resources = self.resources.len(),
285      num_imports = self.import.len(),
286      ?root_config,
287      "initializing component"
288    );
289
290    self.resources.render_config(source.as_deref(), root_config, None)?;
291    self.import.render_config(source.as_deref(), root_config, None)?;
292
293    Ok(self)
294  }
295
296  /// Validate this configuration is good.
297  pub fn validate(&self) -> Result<()> {
298    wick_packet::validation::expect_configuration_matches(
299      self.source().map_or("<unknown>", |p| p.to_str().unwrap_or("<invalid>")),
300      self.root_config.as_ref(),
301      self.config(),
302    )
303    .map_err(Error::ConfigurationInvalid)?;
304    Ok(())
305  }
306}
307
308impl Renderable for ComponentConfiguration {
309  fn render_config(
310    &mut self,
311    source: Option<&Path>,
312    root_config: Option<&RuntimeConfig>,
313    env: Option<&HashMap<String, String>>,
314  ) -> Result<()> {
315    self.resources.render_config(source, root_config, env)?;
316    self.import.render_config(source, root_config, env)?;
317    Ok(())
318  }
319}
320
321impl Lockdown for ComponentConfiguration {
322  fn lockdown(
323    &self,
324    id: Option<&str>,
325    lockdown: &config::LockdownConfiguration,
326  ) -> std::result::Result<(), LockdownError> {
327    let mut errors = Vec::new();
328    let Some(id) = id else {
329      return Err(LockdownError::new(vec![FailureKind::General(
330        "missing component id".into(),
331      )]));
332    };
333
334    if id == Entity::LOCAL {
335      return Err(LockdownError::new(vec![FailureKind::General(format!(
336        "invalid component id: {}",
337        Entity::LOCAL
338      ))]));
339    }
340
341    for resource in &self.resources {
342      if let Err(e) = validate_resource(id, &(resource.into()), lockdown) {
343        errors.push(FailureKind::Failed(Box::new(e)));
344      }
345    }
346
347    if errors.is_empty() {
348      Ok(())
349    } else {
350      Err(LockdownError::new(errors))
351    }
352  }
353}
354
355impl ComponentConfigurationBuilder {
356  #[must_use]
357  /// Initialize a new component configuration builder from an existing configuration.
358  #[allow(clippy::missing_const_for_fn)]
359  pub fn from_base(config: ComponentConfiguration) -> Self {
360    Self {
361      name: Some(config.name),
362      component: Some(config.component),
363      source: None,
364      types: Some(config.types),
365      import: Some(config.import),
366      requires: Some(config.requires),
367      resources: Some(config.resources),
368      host: Some(config.host),
369      tests: Some(config.tests),
370      metadata: Some(config.metadata),
371      package: Some(config.package),
372      root_config: Some(config.root_config),
373      type_cache: std::marker::PhantomData,
374      cached_types: std::marker::PhantomData,
375    }
376  }
377
378  /// Add an imported component to the builder.
379  pub fn add_import(&mut self, import: Binding<ImportDefinition>) {
380    if let Some(imports) = &mut self.import {
381      imports.push(import);
382    } else {
383      self.import = Some(vec![import]);
384    }
385  }
386
387  /// Add an imported resource to the builder.
388  pub fn add_resource(&mut self, resource: Binding<ResourceDefinition>) {
389    if let Some(r) = &mut self.resources {
390      r.push(resource);
391    } else {
392      self.resources = Some(vec![resource]);
393    }
394  }
395
396  /// Build the configuration.
397  pub fn build(self) -> Result<ComponentConfiguration> {
398    let config = self.build_internal()?;
399    config.validate()?;
400    Ok(config)
401  }
402}
403
404impl From<config::Metadata> for ComponentMetadata {
405  fn from(value: config::Metadata) -> Self {
406    Self::new(Some(value.version))
407  }
408}
409
410impl From<config::OperationDefinition> for OperationSignature {
411  fn from(value: config::OperationDefinition) -> Self {
412    Self::new(value.name, value.inputs, value.outputs, value.config)
413  }
414}