Skip to main content

akash_deploy_rs/
state.rs

1//! Deployment state machine definition.
2//!
3//! The state is the complete snapshot of a deployment workflow.
4//! It's serializable, restorable, and the workflow engine doesn't
5//! care how you persist it — that's the backend's problem.
6
7use crate::types::{Bid, LeaseId, ServiceEndpoint};
8use serde::{Deserialize, Serialize};
9
10/// Workflow steps — the state machine's nodes.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Step {
13    /// Starting point.
14    Init,
15    /// Check account has enough AKT.
16    CheckBalance,
17    /// Ensure mTLS certificate exists on chain.
18    EnsureCertificate,
19    /// Create the deployment on chain.
20    CreateDeployment,
21    /// Wait for provider bids.
22    WaitForBids { waited_blocks: u32 },
23    /// Select a provider from available bids.
24    SelectProvider,
25    /// Create lease with selected provider.
26    CreateLease,
27    /// Send manifest to provider.
28    SendManifest,
29    /// Wait for provider to spin up and expose endpoints.
30    WaitForEndpoints { attempts: u32 },
31    /// Done.
32    Complete,
33    /// Failed, possibly recoverable.
34    Failed { reason: String, recoverable: bool },
35}
36
37impl Step {
38    /// Human-readable step name for logging/display.
39    pub fn name(&self) -> &'static str {
40        match self {
41            Step::Init => "init",
42            Step::CheckBalance => "check_balance",
43            Step::EnsureCertificate => "ensure_certificate",
44            Step::CreateDeployment => "create_deployment",
45            Step::WaitForBids { .. } => "wait_for_bids",
46            Step::SelectProvider => "select_provider",
47            Step::CreateLease => "create_lease",
48            Step::SendManifest => "send_manifest",
49            Step::WaitForEndpoints { .. } => "wait_for_endpoints",
50            Step::Complete => "complete",
51            Step::Failed { .. } => "failed",
52        }
53    }
54}
55
56/// Full workflow state — serializable, restorable.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct DeploymentState {
59    /// Unique session identifier.
60    pub session_id: String,
61    /// Current step in the workflow.
62    pub step: Step,
63    /// Account address that owns this deployment.
64    pub owner: String,
65    /// User-defined label for the deployment.
66    pub label: String,
67
68    // Populated as workflow progresses
69    /// SDL content (YAML).
70    pub sdl_content: Option<String>,
71
72    // Template support (feature-gated)
73    #[cfg(feature = "sdl-templates")]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    /// Template variable overrides.
76    pub template_variables: Option<std::collections::HashMap<String, String>>,
77
78    #[cfg(feature = "sdl-templates")]
79    #[serde(skip_serializing_if = "Option::is_none")]
80    /// Template default values.
81    pub template_defaults: Option<std::collections::HashMap<String, String>>,
82
83    #[cfg(feature = "sdl-templates")]
84    #[serde(default)]
85    /// Whether the SDL content is a template.
86    pub is_template: bool,
87    /// Deposit amount in uakt.
88    pub deposit_uakt: u64,
89    /// Deployment sequence number (from chain).
90    pub dseq: Option<u64>,
91    /// Group sequence (usually 1).
92    pub gseq: u32,
93    /// Order sequence (usually 1).
94    pub oseq: u32,
95
96    // Certificate for mTLS
97    /// Certificate PEM (public).
98    pub cert_pem: Option<Vec<u8>>,
99    /// Private key PEM (encrypted or plaintext depending on backend).
100    pub key_pem: Option<Vec<u8>>,
101
102    // Bids and selection
103    /// Available bids from providers.
104    pub bids: Vec<Bid>,
105    /// Selected provider address.
106    pub selected_provider: Option<String>,
107
108    // Result
109    /// Service endpoints after deployment.
110    pub endpoints: Vec<ServiceEndpoint>,
111    /// Lease ID once created.
112    pub lease_id: Option<LeaseId>,
113
114    // Audit
115    /// Unix timestamp of creation.
116    pub created_at: u64,
117    /// Unix timestamp of last update.
118    pub updated_at: u64,
119    /// Transaction hashes for all txs in this workflow.
120    pub tx_hashes: Vec<String>,
121}
122
123impl DeploymentState {
124    /// Create a new deployment state.
125    pub fn new(session_id: impl Into<String>, owner: impl Into<String>) -> Self {
126        let now = current_unix_time();
127
128        Self {
129            session_id: session_id.into(),
130            step: Step::Init,
131            owner: owner.into(),
132            label: String::new(),
133            sdl_content: None,
134            deposit_uakt: 5_000_000, // 5 AKT default
135            dseq: None,
136            gseq: 1,
137            oseq: 1,
138            cert_pem: None,
139            key_pem: None,
140            bids: Vec::new(),
141            selected_provider: None,
142            endpoints: Vec::new(),
143            lease_id: None,
144            created_at: now,
145            updated_at: now,
146            tx_hashes: Vec::new(),
147            #[cfg(feature = "sdl-templates")]
148            template_variables: None,
149            #[cfg(feature = "sdl-templates")]
150            template_defaults: None,
151            #[cfg(feature = "sdl-templates")]
152            is_template: false,
153        }
154    }
155
156    /// Set the label.
157    pub fn with_label(mut self, label: impl Into<String>) -> Self {
158        self.label = label.into();
159        self
160    }
161
162    /// Set the SDL content.
163    pub fn with_sdl(mut self, sdl: impl Into<String>) -> Self {
164        self.sdl_content = Some(sdl.into());
165        self
166    }
167
168    /// Set the deposit amount.
169    pub fn with_deposit(mut self, deposit_uakt: u64) -> Self {
170        self.deposit_uakt = deposit_uakt;
171        self
172    }
173
174    /// Set template defaults and mark as template.
175    #[cfg(feature = "sdl-templates")]
176    pub fn with_template(mut self, defaults: std::collections::HashMap<String, String>) -> Self {
177        self.is_template = true;
178        self.template_defaults = Some(defaults);
179        self
180    }
181
182    /// Set template variable overrides.
183    #[cfg(feature = "sdl-templates")]
184    pub fn with_variables(mut self, variables: std::collections::HashMap<String, String>) -> Self {
185        self.template_variables = Some(variables);
186        self
187    }
188
189    /// Is this workflow in a terminal state?
190    pub fn is_terminal(&self) -> bool {
191        matches!(self.step, Step::Complete | Step::Failed { .. })
192    }
193
194    /// Is this workflow failed?
195    pub fn is_failed(&self) -> bool {
196        matches!(self.step, Step::Failed { .. })
197    }
198
199    /// Is this workflow complete?
200    pub fn is_complete(&self) -> bool {
201        matches!(self.step, Step::Complete)
202    }
203
204    /// Record a transaction hash.
205    pub fn record_tx(&mut self, hash: impl Into<String>) {
206        self.tx_hashes.push(hash.into());
207        self.updated_at = current_unix_time();
208    }
209
210    /// Transition to a new step.
211    pub fn transition(&mut self, step: Step) {
212        self.step = step;
213        self.updated_at = current_unix_time();
214    }
215
216    /// Fail the workflow.
217    pub fn fail(&mut self, reason: impl Into<String>, recoverable: bool) {
218        self.step = Step::Failed {
219            reason: reason.into(),
220            recoverable,
221        };
222        self.updated_at = current_unix_time();
223    }
224}
225
226fn current_unix_time() -> u64 {
227    std::time::SystemTime::now()
228        .duration_since(std::time::UNIX_EPOCH)
229        .unwrap_or_default()
230        .as_secs()
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_new_state() {
239        let state = DeploymentState::new("session-1", "akash1abc...");
240        assert_eq!(state.session_id, "session-1");
241        assert_eq!(state.owner, "akash1abc...");
242        assert!(matches!(state.step, Step::Init));
243        assert!(!state.is_terminal());
244    }
245
246    #[test]
247    fn test_builder_pattern() {
248        let state = DeploymentState::new("s1", "owner")
249            .with_label("test-deploy")
250            .with_sdl("version: 2")
251            .with_deposit(10_000_000);
252
253        assert_eq!(state.label, "test-deploy");
254        assert_eq!(state.sdl_content, Some("version: 2".to_string()));
255        assert_eq!(state.deposit_uakt, 10_000_000);
256    }
257
258    #[test]
259    fn test_terminal_states() {
260        let mut state = DeploymentState::new("s1", "owner");
261        assert!(!state.is_terminal());
262
263        state.transition(Step::Complete);
264        assert!(state.is_terminal());
265        assert!(state.is_complete());
266
267        state.fail("something broke", true);
268        assert!(state.is_terminal());
269        assert!(state.is_failed());
270    }
271
272    #[test]
273    fn test_step_names() {
274        // Test all step name() variants
275        assert_eq!(Step::Init.name(), "init");
276        assert_eq!(Step::CheckBalance.name(), "check_balance");
277        assert_eq!(Step::EnsureCertificate.name(), "ensure_certificate");
278        assert_eq!(Step::CreateDeployment.name(), "create_deployment");
279        assert_eq!(
280            Step::WaitForBids { waited_blocks: 0 }.name(),
281            "wait_for_bids"
282        );
283        assert_eq!(Step::SelectProvider.name(), "select_provider");
284        assert_eq!(Step::CreateLease.name(), "create_lease");
285        assert_eq!(Step::SendManifest.name(), "send_manifest");
286        assert_eq!(
287            Step::WaitForEndpoints { attempts: 0 }.name(),
288            "wait_for_endpoints"
289        );
290        assert_eq!(Step::Complete.name(), "complete");
291        assert_eq!(
292            Step::Failed {
293                reason: "test".to_string(),
294                recoverable: false
295            }
296            .name(),
297            "failed"
298        );
299    }
300}