wasmcloud_control_interface/types/
ctl.rs

1//! Data types used when interacting with the control interface of a wasmCloud lattice
2
3use std::collections::{BTreeMap, HashMap};
4
5use serde::{Deserialize, Serialize};
6
7use crate::Result;
8
9/// A control interface response that wraps a response payload, a success flag, and a message
10/// with additional context if necessary.
11#[derive(Serialize, Deserialize, Debug, Clone)]
12#[non_exhaustive]
13pub struct CtlResponse<T> {
14    /// Whether the request succeeded
15    pub(crate) success: bool,
16    /// A message with additional context about the response
17    pub(crate) message: String,
18    /// The response data, if any
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub(crate) response: Option<T>,
21}
22
23impl<T> CtlResponse<T> {
24    /// Create a [`CtlResponse`] with provided response data
25    #[must_use]
26    pub fn ok(response: T) -> Self {
27        CtlResponse {
28            success: true,
29            message: String::new(),
30            response: Some(response),
31        }
32    }
33
34    /// Get whether the request succeeded
35    #[must_use]
36    pub fn succeeded(&self) -> bool {
37        self.success
38    }
39
40    /// Get the message included in the response
41    #[must_use]
42    pub fn message(&self) -> &str {
43        &self.message
44    }
45
46    /// Get the internal data of the response (if any)
47    #[must_use]
48    pub fn data(&self) -> Option<&T> {
49        self.response.as_ref()
50    }
51
52    /// Take the internal data
53    #[must_use]
54    pub fn into_data(self) -> Option<T> {
55        self.response
56    }
57}
58
59impl CtlResponse<()> {
60    /// Helper function to return a successful response without
61    /// a message or a payload.
62    #[must_use]
63    pub fn success(message: String) -> Self {
64        CtlResponse {
65            success: true,
66            message,
67            response: None,
68        }
69    }
70
71    /// Helper function to return an unsuccessful response with
72    /// a message but no payload. Note that this implicitly is
73    /// typing the inner payload as `()` for efficiency.
74    #[must_use]
75    pub fn error(message: &str) -> Self {
76        CtlResponse {
77            success: false,
78            message: message.to_string(),
79            response: None,
80        }
81    }
82}
83
84/// Command a host to scale a component
85#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
86#[non_exhaustive]
87pub struct ScaleComponentCommand {
88    /// Image reference for the component.
89    #[serde(default)]
90    pub(crate) component_ref: String,
91    /// Unique identifier of the component to scale.
92    pub(crate) component_id: String,
93    /// Optional set of annotations used to describe the nature of this component scale command. For
94    /// example, autonomous agents may wish to "tag" scale requests as part of a given deployment
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub(crate) annotations: Option<BTreeMap<String, String>>,
97    /// The maximum number of concurrent executing instances of this component. Setting this to `0` will
98    /// stop the component.
99    // NOTE: renaming to `count` lets us remain backwards compatible for a few minor versions
100    #[serde(default, alias = "count", rename = "count")]
101    pub(crate) max_instances: u32,
102    /// The Component Limits
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub(crate) component_limits: Option<HashMap<String, String>>,
105    /// Host ID on which to scale this component
106    #[serde(default)]
107    pub(crate) host_id: String,
108    /// A list of named configs to use for this component. It is not required to specify a config.
109    /// Configs are merged together before being given to the component, with values from the right-most
110    /// config in the list taking precedence. For example, given ordered configs foo {a: 1, b: 2},
111    /// bar {b: 3, c: 4}, and baz {c: 5, d: 6}, the resulting config will be: {a: 1, b: 3, c: 5, d:
112    /// 6}
113    #[serde(default)]
114    pub(crate) config: Vec<String>,
115    #[serde(default)]
116    /// Whether to perform an update if the details of the component (ex. component ID) change as
117    /// part of the scale request.
118    ///
119    /// Normally this is implemented by the receiver (ex. wasmcloud host) as a *separate* update component call
120    /// being made shortly after this command (scale) is processed.
121    pub(crate) allow_update: bool,
122}
123
124impl ScaleComponentCommand {
125    #[must_use]
126    pub fn component_ref(&self) -> &str {
127        &self.component_ref
128    }
129
130    #[must_use]
131    pub fn component_id(&self) -> &str {
132        &self.component_id
133    }
134
135    #[must_use]
136    pub fn allow_update(&self) -> bool {
137        self.allow_update
138    }
139
140    #[must_use]
141    pub fn config(&self) -> &Vec<String> {
142        &self.config
143    }
144
145    #[must_use]
146    pub fn annotations(&self) -> Option<&BTreeMap<String, String>> {
147        self.annotations.as_ref()
148    }
149
150    #[must_use]
151    pub fn max_instances(&self) -> u32 {
152        self.max_instances
153    }
154
155    #[must_use]
156    pub fn component_limits(&self) -> Option<HashMap<String, String>> {
157        self.component_limits.clone()
158    }
159
160    #[must_use]
161    pub fn host_id(&self) -> &str {
162        &self.host_id
163    }
164
165    #[must_use]
166    pub fn builder() -> ScaleComponentCommandBuilder {
167        ScaleComponentCommandBuilder::default()
168    }
169}
170
171/// Builder that produces [`ScaleComponentCommand`]s
172#[derive(Clone, Debug, Default, Eq, PartialEq)]
173#[non_exhaustive]
174pub struct ScaleComponentCommandBuilder {
175    component_ref: Option<String>,
176    component_id: Option<String>,
177    annotations: Option<BTreeMap<String, String>>,
178    max_instances: Option<u32>,
179    component_limits: Option<HashMap<String, String>>,
180    host_id: Option<String>,
181    config: Option<Vec<String>>,
182    allow_update: Option<bool>,
183}
184
185impl ScaleComponentCommandBuilder {
186    #[must_use]
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    #[must_use]
192    pub fn component_ref(mut self, v: &str) -> Self {
193        self.component_ref = Some(v.into());
194        self
195    }
196
197    #[must_use]
198    pub fn component_id(mut self, v: &str) -> Self {
199        self.component_id = Some(v.into());
200        self
201    }
202
203    #[must_use]
204    pub fn annotations(mut self, v: impl Into<BTreeMap<String, String>>) -> Self {
205        self.annotations = Some(v.into());
206        self
207    }
208
209    #[must_use]
210    pub fn max_instances(mut self, v: u32) -> Self {
211        self.max_instances = Some(v);
212        self
213    }
214
215    #[must_use]
216    pub fn component_limits(mut self, v: Option<HashMap<String, String>>) -> Self {
217        self.component_limits = v;
218        self
219    }
220
221    #[must_use]
222    pub fn host_id(mut self, v: &str) -> Self {
223        self.host_id = Some(v.into());
224        self
225    }
226
227    #[must_use]
228    pub fn config(mut self, v: Vec<String>) -> Self {
229        self.config = Some(v);
230        self
231    }
232
233    #[must_use]
234    pub fn allow_update(mut self, v: bool) -> Self {
235        self.allow_update = Some(v);
236        self
237    }
238
239    pub fn build(self) -> Result<ScaleComponentCommand> {
240        Ok(ScaleComponentCommand {
241            component_ref: self
242                .component_ref
243                .ok_or_else(|| "component ref is required for scaling components".to_string())?,
244            component_id: self
245                .component_id
246                .ok_or_else(|| "component id is required for scaling components".to_string())?,
247            annotations: self.annotations,
248            max_instances: self.max_instances.unwrap_or(0),
249            component_limits: self.component_limits,
250            host_id: self
251                .host_id
252                .ok_or_else(|| "host id is required for scaling hosts host".to_string())?,
253            config: self.config.unwrap_or_default(),
254            allow_update: self.allow_update.unwrap_or_default(),
255        })
256    }
257}
258
259/// A command sent to a host requesting a capability provider be started with the
260/// given link name and optional configuration.
261#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
262#[non_exhaustive]
263pub struct StartProviderCommand {
264    /// Unique identifier of the provider to start.
265    provider_id: String,
266    /// The image reference of the provider to be started
267    #[serde(default)]
268    provider_ref: String,
269    /// The host ID on which to start the provider
270    #[serde(default)]
271    host_id: String,
272    /// A list of named configs to use for this provider. It is not required to specify a config.
273    /// Configs are merged together before being given to the provider, with values from the right-most
274    /// config in the list taking precedence. For example, given ordered configs foo {a: 1, b: 2},
275    /// bar {b: 3, c: 4}, and baz {c: 5, d: 6}, the resulting config will be: {a: 1, b: 3, c: 5, d:
276    /// 6}
277    #[serde(default)]
278    config: Vec<String>,
279    /// Optional set of annotations used to describe the nature of this provider start command. For
280    /// example, autonomous agents may wish to "tag" start requests as part of a given deployment
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    annotations: Option<BTreeMap<String, String>>,
283}
284
285impl StartProviderCommand {
286    #[must_use]
287    pub fn provider_id(&self) -> &str {
288        &self.provider_id
289    }
290
291    #[must_use]
292    pub fn provider_ref(&self) -> &str {
293        &self.provider_ref
294    }
295
296    #[must_use]
297    pub fn host_id(&self) -> &str {
298        &self.host_id
299    }
300
301    #[must_use]
302    pub fn config(&self) -> &Vec<String> {
303        &self.config
304    }
305
306    #[must_use]
307    pub fn annotations(&self) -> Option<&BTreeMap<String, String>> {
308        self.annotations.as_ref()
309    }
310
311    #[must_use]
312    pub fn builder() -> StartProviderCommandBuilder {
313        StartProviderCommandBuilder::default()
314    }
315}
316
317/// A builder that produces [`StartProviderCommand`]s
318#[derive(Clone, Debug, Default, Eq, PartialEq)]
319#[non_exhaustive]
320pub struct StartProviderCommandBuilder {
321    host_id: Option<String>,
322    provider_id: Option<String>,
323    provider_ref: Option<String>,
324    annotations: Option<BTreeMap<String, String>>,
325    config: Option<Vec<String>>,
326}
327
328impl StartProviderCommandBuilder {
329    #[must_use]
330    pub fn new() -> Self {
331        Self::default()
332    }
333
334    #[must_use]
335    pub fn provider_ref(mut self, v: &str) -> Self {
336        self.provider_ref = Some(v.into());
337        self
338    }
339
340    #[must_use]
341    pub fn provider_id(mut self, v: &str) -> Self {
342        self.provider_id = Some(v.into());
343        self
344    }
345
346    #[must_use]
347    pub fn annotations(mut self, v: impl Into<BTreeMap<String, String>>) -> Self {
348        self.annotations = Some(v.into());
349        self
350    }
351
352    #[must_use]
353    pub fn host_id(mut self, v: &str) -> Self {
354        self.host_id = Some(v.into());
355        self
356    }
357
358    #[must_use]
359    pub fn config(mut self, v: Vec<String>) -> Self {
360        self.config = Some(v);
361        self
362    }
363
364    pub fn build(self) -> Result<StartProviderCommand> {
365        Ok(StartProviderCommand {
366            provider_ref: self
367                .provider_ref
368                .ok_or_else(|| "provider ref is required for starting providers".to_string())?,
369            provider_id: self
370                .provider_id
371                .ok_or_else(|| "provider id is required for starting providers".to_string())?,
372            annotations: self.annotations,
373            host_id: self
374                .host_id
375                .ok_or_else(|| "host id is required for starting providers".to_string())?,
376            config: self.config.unwrap_or_default(),
377        })
378    }
379}
380
381/// A command sent to request that the given host purge and stop
382#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
383#[non_exhaustive]
384pub struct StopHostCommand {
385    /// The ID of the target host
386    #[serde(default)]
387    pub(crate) host_id: String,
388    /// An optional timeout, in seconds
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub(crate) timeout: Option<u64>,
391}
392
393impl StopHostCommand {
394    #[must_use]
395    pub fn host_id(&self) -> &str {
396        &self.host_id
397    }
398
399    #[must_use]
400    pub fn timeout(&self) -> Option<u64> {
401        self.timeout
402    }
403
404    #[must_use]
405    pub fn builder() -> StopHostCommandBuilder {
406        StopHostCommandBuilder::default()
407    }
408}
409
410#[derive(Clone, Debug, Default, Eq, PartialEq)]
411#[non_exhaustive]
412pub struct StopHostCommandBuilder {
413    host_id: Option<String>,
414    timeout: Option<u64>,
415}
416
417impl StopHostCommandBuilder {
418    #[must_use]
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    #[must_use]
424    pub fn host_id(mut self, v: &str) -> Self {
425        self.host_id = Some(v.into());
426        self
427    }
428
429    #[must_use]
430    pub fn timeout(mut self, v: u64) -> Self {
431        self.timeout = Some(v);
432        self
433    }
434
435    pub fn build(self) -> Result<StopHostCommand> {
436        Ok(StopHostCommand {
437            host_id: self
438                .host_id
439                .ok_or_else(|| "host id is required for stopping host".to_string())?,
440            timeout: self.timeout,
441        })
442    }
443}
444
445/// A request to stop the given provider on the indicated host
446#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
447#[non_exhaustive]
448pub struct StopProviderCommand {
449    /// Host ID on which to stop the provider
450    #[serde(default)]
451    pub(crate) host_id: String,
452    /// Unique identifier for the provider to stop.
453    #[serde(default, alias = "provider_ref")]
454    pub(crate) provider_id: String,
455}
456
457impl StopProviderCommand {
458    #[must_use]
459    pub fn host_id(&self) -> &str {
460        &self.host_id
461    }
462
463    #[must_use]
464    pub fn provider_id(&self) -> &str {
465        &self.provider_id
466    }
467
468    #[must_use]
469    pub fn builder() -> StopProviderCommandBuilder {
470        StopProviderCommandBuilder::default()
471    }
472}
473
474/// Builder for [`StopProviderCommand`]s
475#[derive(Clone, Debug, Default, Eq, PartialEq)]
476#[non_exhaustive]
477pub struct StopProviderCommandBuilder {
478    host_id: Option<String>,
479    provider_id: Option<String>,
480}
481
482impl StopProviderCommandBuilder {
483    #[must_use]
484    pub fn new() -> Self {
485        Self::default()
486    }
487
488    #[must_use]
489    pub fn host_id(mut self, v: &str) -> Self {
490        self.host_id = Some(v.into());
491        self
492    }
493
494    #[must_use]
495    pub fn provider_id(mut self, v: &str) -> Self {
496        self.provider_id = Some(v.into());
497        self
498    }
499
500    pub fn build(self) -> Result<StopProviderCommand> {
501        Ok(StopProviderCommand {
502            host_id: self
503                .host_id
504                .ok_or_else(|| "host id is required for stopping provider".to_string())?,
505            provider_id: self
506                .provider_id
507                .ok_or_else(|| "provider id is required for stopping provider".to_string())?,
508        })
509    }
510}
511
512/// A command instructing a specific host to perform a live update
513/// on the indicated component by supplying a new image reference. Note that
514/// live updates are only possible through image references
515#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
516#[non_exhaustive]
517pub struct UpdateComponentCommand {
518    /// The component's 56-character unique ID
519    #[serde(default)]
520    pub(crate) component_id: String,
521    /// Optional set of annotations used to describe the nature of this
522    /// update request. Only component instances that have matching annotations
523    /// will be upgraded, allowing for instance isolation by
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub(crate) annotations: Option<BTreeMap<String, String>>,
526    /// The host ID of the host to perform the live update
527    #[serde(default)]
528    pub(crate) host_id: String,
529    /// The new image reference of the upgraded version of this component
530    #[serde(default)]
531    pub(crate) new_component_ref: String,
532}
533
534impl UpdateComponentCommand {
535    #[must_use]
536    pub fn host_id(&self) -> &str {
537        &self.host_id
538    }
539
540    #[must_use]
541    pub fn component_id(&self) -> &str {
542        &self.component_id
543    }
544
545    #[must_use]
546    pub fn new_component_ref(&self) -> &str {
547        &self.new_component_ref
548    }
549
550    #[must_use]
551    pub fn annotations(&self) -> Option<&BTreeMap<String, String>> {
552        self.annotations.as_ref()
553    }
554
555    #[must_use]
556    pub fn builder() -> UpdateComponentCommandBuilder {
557        UpdateComponentCommandBuilder::default()
558    }
559}
560
561/// Builder for [`UpdateComponentCommand`]s
562#[derive(Clone, Debug, Default, Eq, PartialEq)]
563#[non_exhaustive]
564pub struct UpdateComponentCommandBuilder {
565    host_id: Option<String>,
566    component_id: Option<String>,
567    new_component_ref: Option<String>,
568    annotations: Option<BTreeMap<String, String>>,
569}
570
571impl UpdateComponentCommandBuilder {
572    #[must_use]
573    pub fn new() -> Self {
574        Self::default()
575    }
576
577    #[must_use]
578    pub fn host_id(mut self, v: &str) -> Self {
579        self.host_id = Some(v.into());
580        self
581    }
582
583    #[must_use]
584    pub fn component_id(mut self, v: &str) -> Self {
585        self.component_id = Some(v.into());
586        self
587    }
588
589    #[must_use]
590    pub fn new_component_ref(mut self, v: &str) -> Self {
591        self.new_component_ref = Some(v.into());
592        self
593    }
594
595    #[must_use]
596    pub fn annotations(mut self, v: impl Into<BTreeMap<String, String>>) -> Self {
597        self.annotations = Some(v.into());
598        self
599    }
600
601    pub fn build(self) -> Result<UpdateComponentCommand> {
602        Ok(UpdateComponentCommand {
603            host_id: self
604                .host_id
605                .ok_or_else(|| "host id is required for updating components".to_string())?,
606            component_id: self
607                .component_id
608                .ok_or_else(|| "component id is required for updating components".to_string())?,
609            new_component_ref: self.new_component_ref.ok_or_else(|| {
610                "new component ref is required for updating components".to_string()
611            })?,
612            annotations: self.annotations,
613        })
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use std::collections::BTreeMap;
620
621    use super::{
622        ScaleComponentCommand, StartProviderCommand, StopHostCommand, StopProviderCommand,
623        UpdateComponentCommand,
624    };
625
626    #[test]
627    fn scale_component_command_builder() {
628        assert_eq!(
629            ScaleComponentCommand {
630                component_ref: "component_ref".into(),
631                component_id: "component_id".into(),
632                host_id: "host_id".into(),
633                config: vec!["c".into()],
634                allow_update: true,
635                annotations: Some(BTreeMap::from([("a".into(), "b".into())])),
636                max_instances: 1,
637                component_limits: None,
638            },
639            ScaleComponentCommand::builder()
640                .component_ref("component_ref")
641                .component_id("component_id")
642                .host_id("host_id")
643                .config(vec!["c".into()])
644                .allow_update(true)
645                .annotations(BTreeMap::from([("a".into(), "b".into())]))
646                .max_instances(1)
647                .build()
648                .unwrap()
649        )
650    }
651
652    #[test]
653    fn start_provider_command_builder() {
654        assert_eq!(
655            StartProviderCommand {
656                provider_id: "provider_id".into(),
657                provider_ref: "provider_ref".into(),
658                host_id: "host_id".into(),
659                config: vec!["p".into()],
660                annotations: Some(BTreeMap::from([("a".into(), "b".into())])),
661            },
662            StartProviderCommand::builder()
663                .provider_id("provider_id")
664                .provider_ref("provider_ref")
665                .host_id("host_id")
666                .config(vec!["p".into()])
667                .annotations(BTreeMap::from([("a".into(), "b".into())]))
668                .build()
669                .unwrap()
670        )
671    }
672
673    #[test]
674    fn stop_host_command_builder() {
675        assert_eq!(
676            StopHostCommand {
677                host_id: "host_id".into(),
678                timeout: Some(1),
679            },
680            StopHostCommand::builder()
681                .host_id("host_id")
682                .timeout(1)
683                .build()
684                .unwrap()
685        )
686    }
687
688    #[test]
689    fn stop_provider_command_builder() {
690        assert_eq!(
691            StopProviderCommand {
692                host_id: "host_id".into(),
693                provider_id: "provider_id".into(),
694            },
695            StopProviderCommand::builder()
696                .provider_id("provider_id")
697                .host_id("host_id")
698                .build()
699                .unwrap()
700        )
701    }
702
703    #[test]
704    fn update_component_command_builder() {
705        assert_eq!(
706            UpdateComponentCommand {
707                host_id: "host_id".into(),
708                component_id: "component_id".into(),
709                new_component_ref: "new_component_ref".into(),
710                annotations: Some(BTreeMap::from([("a".into(), "b".into())])),
711            },
712            UpdateComponentCommand::builder()
713                .host_id("host_id")
714                .component_id("component_id")
715                .new_component_ref("new_component_ref")
716                .annotations(BTreeMap::from([("a".into(), "b".into())]))
717                .build()
718                .unwrap()
719        )
720    }
721}