wick_config/config/common/
resources.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use asset_container::{Asset, AssetFlags};
5use url::Url;
6use wick_asset_reference::AssetReference;
7use wick_packet::RuntimeConfig;
8
9use super::template_config::Renderable;
10use crate::config::TemplateConfig;
11use crate::error::ManifestError;
12
13crate::impl_from_for!(ResourceDefinition, TcpPort);
14crate::impl_from_for!(ResourceDefinition, UdpPort);
15crate::impl_from_for!(ResourceDefinition, Volume);
16crate::impl_from_for!(ResourceDefinition, Url, UrlResource);
17
18/// A resource type.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ResourceKind {
21  TcpPort,
22  UdpPort,
23  Url,
24  Volume,
25}
26
27impl std::fmt::Display for ResourceKind {
28  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29    match self {
30      Self::TcpPort => write!(f, "TcpPort"),
31      Self::UdpPort => write!(f, "UdpPort"),
32      Self::Url => write!(f, "Url"),
33      Self::Volume => write!(f, "Volume"),
34    }
35  }
36}
37
38#[derive(Debug, Clone, derive_asset_container::AssetManager, serde::Serialize, PartialEq, Hash, Eq)]
39#[asset(asset(AssetReference))]
40/// Normalized representation of a resource definition.
41#[serde(rename_all = "kebab-case")]
42pub enum ResourceDefinition {
43  /// A TCP port.
44  #[asset(skip)]
45  TcpPort(TcpPort),
46  /// A UDP port.
47  #[asset(skip)]
48  UdpPort(UdpPort),
49  /// A URL resource.
50  #[asset(skip)]
51  Url(UrlResource),
52  /// A filesystem or network volume.
53  Volume(Volume),
54}
55
56impl Renderable for ResourceDefinition {
57  fn render_config(
58    &mut self,
59    source: Option<&Path>,
60    root_config: Option<&RuntimeConfig>,
61    env: Option<&HashMap<String, String>>,
62  ) -> Result<(), ManifestError> {
63    match self {
64      ResourceDefinition::TcpPort(v) => v.render_config(source, root_config, env),
65      ResourceDefinition::UdpPort(v) => v.render_config(source, root_config, env),
66      ResourceDefinition::Url(v) => v.render_config(source, root_config, env),
67      ResourceDefinition::Volume(v) => v.render_config(source, root_config, env),
68    }
69  }
70}
71
72impl ResourceDefinition {
73  #[must_use]
74  pub const fn kind(&self) -> ResourceKind {
75    match self {
76      ResourceDefinition::TcpPort(_) => ResourceKind::TcpPort,
77      ResourceDefinition::UdpPort(_) => ResourceKind::UdpPort,
78      ResourceDefinition::Url(_) => ResourceKind::Url,
79      ResourceDefinition::Volume(_) => ResourceKind::Volume,
80    }
81  }
82
83  pub fn try_tcpport(self) -> Result<TcpPort, ManifestError> {
84    self.try_into()
85  }
86
87  pub fn try_udpport(self) -> Result<UdpPort, ManifestError> {
88    self.try_into()
89  }
90
91  pub fn try_url(self) -> Result<UrlResource, ManifestError> {
92    self.try_into()
93  }
94
95  pub fn try_volume(self) -> Result<Volume, ManifestError> {
96    self.try_into()
97  }
98}
99
100impl TryFrom<String> for UrlResource {
101  type Error = crate::Error;
102
103  fn try_from(value: String) -> Result<Self, Self::Error> {
104    url::Url::parse(&value)
105      .map_err(|_| Self::Error::InvalidUrl(value.clone()))
106      .map(Self::new)
107  }
108}
109
110#[derive(Debug, Clone, PartialEq, Hash, Eq, derive_builder::Builder, serde::Serialize)]
111/// A filesystem or network volume.
112#[must_use]
113pub struct Volume {
114  path: TemplateConfig<AssetReference>,
115}
116
117impl Volume {
118  pub fn new(template: String) -> Self {
119    let template = TemplateConfig::new_template(template);
120    Self { path: template }
121  }
122
123  #[cfg(feature = "v1")]
124  pub(crate) fn unrender(&self) -> Result<String, ManifestError> {
125    self.path.unrender()
126  }
127
128  pub fn path(&self) -> Result<PathBuf, ManifestError> {
129    if let Some(path) = &self.path.value {
130      Ok(path.path()?)
131    } else {
132      Err(ManifestError::UnrenderedConfiguration(format!(
133        "{:?}",
134        self.path.template
135      )))
136    }
137  }
138}
139
140impl Renderable for Volume {
141  fn render_config(
142    &mut self,
143    source: Option<&Path>,
144    root_config: Option<&RuntimeConfig>,
145    env: Option<&HashMap<String, String>>,
146  ) -> Result<(), ManifestError> {
147    self.path.set_value(self.path.render(source, root_config, env)?);
148    Ok(())
149  }
150}
151
152impl asset_container::AssetManager for Volume {
153  type Asset = AssetReference;
154
155  fn assets(&self) -> asset_container::Assets<Self::Asset> {
156    asset_container::Assets::new(
157      self
158        .path
159        .value
160        .as_ref()
161        .map_or_else(Vec::new, |v| vec![std::borrow::Cow::Borrowed(v)]),
162      self.get_asset_flags(),
163    )
164  }
165
166  fn set_baseurl(&self, baseurl: &Path) {
167    if let Some(path) = &self.path.value {
168      path.update_baseurl(baseurl);
169      match self.path() {
170        Ok(p) => {
171          if !p.is_dir() {
172            tracing::warn!(%path,"volume path is not a directory");
173          }
174        }
175        Err(e) => {
176          tracing::warn!(%path,error=%e,"volume path could not be resolved");
177        }
178      }
179    }
180  }
181
182  fn get_asset_flags(&self) -> u32 {
183    AssetFlags::Lazy.bits()
184  }
185}
186
187#[derive(Debug, Clone, PartialEq, Hash, Eq, property::Property, serde::Serialize)]
188/// A URL resource.
189#[must_use]
190#[property(get(public), set(private), mut(disable))]
191pub struct UrlResource {
192  /// The URL
193  pub(crate) url: TemplateConfig<Url>,
194}
195
196impl UrlResource {
197  /// Create a new URL Resource.
198  pub const fn new(url: Url) -> Self {
199    Self {
200      url: TemplateConfig::new_value(url),
201    }
202  }
203
204  /// Get the URL.
205  #[must_use]
206  #[allow(clippy::missing_const_for_fn)]
207  pub fn into_inner(self) -> TemplateConfig<Url> {
208    self.url
209  }
210}
211
212impl std::fmt::Display for UrlResource {
213  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214    write!(f, "{}", self.url)
215  }
216}
217
218impl Renderable for UrlResource {
219  fn render_config(
220    &mut self,
221    source: Option<&Path>,
222    root_config: Option<&RuntimeConfig>,
223    env: Option<&HashMap<String, String>>,
224  ) -> Result<(), ManifestError> {
225    self.url.set_value(self.url.render(source, root_config, env)?);
226    Ok(())
227  }
228}
229
230#[derive(Debug, Clone, PartialEq, Hash, Eq, property::Property, serde::Serialize)]
231/// Normalized representation of a TCP port configuration.
232#[property(get(public), set(private), mut(disable))]
233pub struct TcpPort {
234  /// The port number.
235  pub(crate) port: TemplateConfig<u16>,
236  /// The address to bind to.
237  pub(crate) host: TemplateConfig<String>,
238}
239
240impl TcpPort {
241  /// Create a new TCP port configuration.
242  pub fn new<T: Into<String>>(host: T, port: u16) -> Self {
243    Self {
244      port: TemplateConfig::new_value(port),
245      host: TemplateConfig::new_value(host.into()),
246    }
247  }
248
249  /// Get the address and port as a string.
250  #[must_use]
251  pub fn address(&self) -> String {
252    format!("{}:{}", self.host, self.port)
253  }
254}
255
256impl Renderable for TcpPort {
257  fn render_config(
258    &mut self,
259    source: Option<&Path>,
260    root_config: Option<&RuntimeConfig>,
261    env: Option<&HashMap<String, String>>,
262  ) -> Result<(), ManifestError> {
263    self.port.set_value(self.port.render(source, root_config, env)?);
264    self.host.set_value(self.host.render(source, root_config, env)?);
265    Ok(())
266  }
267}
268
269#[derive(Debug, Clone, PartialEq, Hash, Eq, property::Property, serde::Serialize)]
270/// Normalized representation of a UDP port configuration.
271#[property(get(public), set(private), mut(disable))]
272pub struct UdpPort {
273  /// The port number.
274  pub(crate) port: TemplateConfig<u16>,
275  /// The address to bind to.
276  pub(crate) host: TemplateConfig<String>,
277}
278
279impl UdpPort {
280  /// Create a new UDP port configuration.
281  pub fn new<T: Into<String>>(host: T, port: u16) -> Self {
282    Self {
283      port: TemplateConfig::new_value(port),
284      host: TemplateConfig::new_value(host.into()),
285    }
286  }
287
288  /// Get the address and port as a string.
289  #[must_use]
290  pub fn address(&self) -> String {
291    format!("{}:{}", self.host, self.port)
292  }
293}
294
295impl Renderable for UdpPort {
296  fn render_config(
297    &mut self,
298    source: Option<&Path>,
299    root_config: Option<&RuntimeConfig>,
300    env: Option<&HashMap<String, String>>,
301  ) -> Result<(), ManifestError> {
302    self.port.set_value(self.port.render(source, root_config, env)?);
303    self.host.set_value(self.host.render(source, root_config, env)?);
304    Ok(())
305  }
306}
307
308#[cfg(test)]
309mod test {
310  use anyhow::Result;
311
312  use super::*;
313
314  #[test]
315  fn try_into_urlresource() -> Result<()> {
316    let urlstr = "postgres://postgres:password!12345@nas.glhf.lan:55432/wick_test".to_owned();
317    let url_resource: UrlResource = urlstr.clone().try_into()?;
318    let url: Url = urlstr.parse()?;
319
320    assert_eq!(url_resource.url.value_unchecked().as_str(), &urlstr);
321    assert_eq!(url_resource.url.value_unchecked(), &url);
322
323    Ok(())
324  }
325}