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