Skip to main content

talos_api_rs/resources/
upgrade.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for the Upgrade API.
4//!
5//! Provides functionality to upgrade Talos nodes to a new version.
6
7use crate::api::generated::machine::{
8    Upgrade as ProtoUpgrade, UpgradeRequest as ProtoUpgradeRequest,
9    UpgradeResponse as ProtoUpgradeResponse,
10};
11
12/// Reboot mode for upgrade.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum UpgradeRebootMode {
15    /// Default reboot mode.
16    #[default]
17    Default,
18    /// Power cycle instead of reboot.
19    PowerCycle,
20}
21
22impl From<UpgradeRebootMode> for i32 {
23    fn from(mode: UpgradeRebootMode) -> Self {
24        match mode {
25            UpgradeRebootMode::Default => 0,
26            UpgradeRebootMode::PowerCycle => 1,
27        }
28    }
29}
30
31impl std::fmt::Display for UpgradeRebootMode {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            UpgradeRebootMode::Default => write!(f, "default"),
35            UpgradeRebootMode::PowerCycle => write!(f, "powercycle"),
36        }
37    }
38}
39
40/// Request to upgrade a Talos node.
41///
42/// # Example
43///
44/// ```no_run
45/// use talos_api_rs::resources::UpgradeRequest;
46///
47/// // Upgrade to a specific version
48/// let request = UpgradeRequest::new("ghcr.io/siderolabs/installer:v1.6.0");
49///
50/// // Staged upgrade (downloads image but doesn't apply until reboot)
51/// let request = UpgradeRequest::builder("ghcr.io/siderolabs/installer:v1.6.0")
52///     .stage(true)
53///     .preserve(true)
54///     .build();
55/// ```
56#[derive(Debug, Clone)]
57pub struct UpgradeRequest {
58    /// The upgrade image reference.
59    pub image: String,
60    /// Preserve data across the upgrade.
61    pub preserve: bool,
62    /// Stage the upgrade (don't apply immediately).
63    pub stage: bool,
64    /// Force upgrade even if already on same version.
65    pub force: bool,
66    /// Reboot mode.
67    pub reboot_mode: UpgradeRebootMode,
68}
69
70impl UpgradeRequest {
71    /// Create a new upgrade request with the given image.
72    #[must_use]
73    pub fn new(image: impl Into<String>) -> Self {
74        Self {
75            image: image.into(),
76            preserve: false,
77            stage: false,
78            force: false,
79            reboot_mode: UpgradeRebootMode::Default,
80        }
81    }
82
83    /// Create a builder for customizing the upgrade request.
84    #[must_use]
85    pub fn builder(image: impl Into<String>) -> UpgradeRequestBuilder {
86        UpgradeRequestBuilder::new(image)
87    }
88}
89
90impl From<UpgradeRequest> for ProtoUpgradeRequest {
91    fn from(req: UpgradeRequest) -> Self {
92        Self {
93            image: req.image,
94            preserve: req.preserve,
95            stage: req.stage,
96            force: req.force,
97            reboot_mode: req.reboot_mode.into(),
98        }
99    }
100}
101
102/// Builder for UpgradeRequest.
103#[derive(Debug, Clone)]
104pub struct UpgradeRequestBuilder {
105    image: String,
106    preserve: bool,
107    stage: bool,
108    force: bool,
109    reboot_mode: UpgradeRebootMode,
110}
111
112impl UpgradeRequestBuilder {
113    /// Create a new builder with the given image.
114    #[must_use]
115    pub fn new(image: impl Into<String>) -> Self {
116        Self {
117            image: image.into(),
118            preserve: false,
119            stage: false,
120            force: false,
121            reboot_mode: UpgradeRebootMode::Default,
122        }
123    }
124
125    /// Preserve data across the upgrade.
126    #[must_use]
127    pub fn preserve(mut self, preserve: bool) -> Self {
128        self.preserve = preserve;
129        self
130    }
131
132    /// Stage the upgrade (downloads image but doesn't apply until reboot).
133    #[must_use]
134    pub fn stage(mut self, stage: bool) -> Self {
135        self.stage = stage;
136        self
137    }
138
139    /// Force upgrade even if already on same version.
140    #[must_use]
141    pub fn force(mut self, force: bool) -> Self {
142        self.force = force;
143        self
144    }
145
146    /// Set the reboot mode.
147    #[must_use]
148    pub fn reboot_mode(mut self, mode: UpgradeRebootMode) -> Self {
149        self.reboot_mode = mode;
150        self
151    }
152
153    /// Build the request.
154    #[must_use]
155    pub fn build(self) -> UpgradeRequest {
156        UpgradeRequest {
157            image: self.image,
158            preserve: self.preserve,
159            stage: self.stage,
160            force: self.force,
161            reboot_mode: self.reboot_mode,
162        }
163    }
164}
165
166/// Result from an upgrade operation.
167#[derive(Debug, Clone)]
168pub struct UpgradeResult {
169    /// Node that processed the upgrade.
170    pub node: Option<String>,
171    /// Acknowledgement message.
172    pub ack: String,
173    /// Actor ID that triggered the upgrade.
174    pub actor_id: String,
175}
176
177impl From<ProtoUpgrade> for UpgradeResult {
178    fn from(proto: ProtoUpgrade) -> Self {
179        Self {
180            node: proto.metadata.map(|m| m.hostname),
181            ack: proto.ack,
182            actor_id: proto.actor_id,
183        }
184    }
185}
186
187/// Response from an upgrade operation.
188#[derive(Debug, Clone)]
189pub struct UpgradeResponse {
190    /// Results from each node.
191    pub results: Vec<UpgradeResult>,
192}
193
194impl From<ProtoUpgradeResponse> for UpgradeResponse {
195    fn from(proto: ProtoUpgradeResponse) -> Self {
196        Self {
197            results: proto
198                .messages
199                .into_iter()
200                .map(UpgradeResult::from)
201                .collect(),
202        }
203    }
204}
205
206impl UpgradeResponse {
207    /// Check if the upgrade was initiated successfully.
208    #[must_use]
209    pub fn is_success(&self) -> bool {
210        !self.results.is_empty()
211    }
212
213    /// Get the first result.
214    #[must_use]
215    pub fn first(&self) -> Option<&UpgradeResult> {
216        self.results.first()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_upgrade_request_new() {
226        let req = UpgradeRequest::new("ghcr.io/siderolabs/installer:v1.6.0");
227        assert_eq!(req.image, "ghcr.io/siderolabs/installer:v1.6.0");
228        assert!(!req.preserve);
229        assert!(!req.stage);
230        assert!(!req.force);
231        assert_eq!(req.reboot_mode, UpgradeRebootMode::Default);
232    }
233
234    #[test]
235    fn test_upgrade_request_builder() {
236        let req = UpgradeRequest::builder("ghcr.io/siderolabs/installer:v1.6.0")
237            .preserve(true)
238            .stage(true)
239            .force(true)
240            .reboot_mode(UpgradeRebootMode::PowerCycle)
241            .build();
242
243        assert_eq!(req.image, "ghcr.io/siderolabs/installer:v1.6.0");
244        assert!(req.preserve);
245        assert!(req.stage);
246        assert!(req.force);
247        assert_eq!(req.reboot_mode, UpgradeRebootMode::PowerCycle);
248    }
249
250    #[test]
251    fn test_upgrade_reboot_mode() {
252        assert_eq!(i32::from(UpgradeRebootMode::Default), 0);
253        assert_eq!(i32::from(UpgradeRebootMode::PowerCycle), 1);
254        assert_eq!(UpgradeRebootMode::PowerCycle.to_string(), "powercycle");
255    }
256
257    #[test]
258    fn test_proto_conversion() {
259        let req = UpgradeRequest::builder("test:v1.0")
260            .stage(true)
261            .force(true)
262            .build();
263
264        let proto: ProtoUpgradeRequest = req.into();
265        assert_eq!(proto.image, "test:v1.0");
266        assert!(proto.stage);
267        assert!(proto.force);
268    }
269}