Skip to main content

tonin_client/
platform_client.rs

1//! Platform client for agnitiv-platform to trigger tonin deployments.
2//!
3//! This module provides a client interface for orchestrating tonin deployments
4//! without directly calling the tonin CLI. Can be extended to support gRPC
5//! in future phases.
6
7use serde::{Deserialize, Serialize};
8use std::process::Command;
9
10/// Configuration for the platform client.
11#[derive(Debug, Clone)]
12pub struct PlatformClientConfig {
13    /// Path to tonin binary or "tonin" if on $PATH
14    pub tonin_bin: String,
15    /// Workspace root directory
16    pub workspace: String,
17}
18
19impl Default for PlatformClientConfig {
20    fn default() -> Self {
21        Self {
22            tonin_bin: "tonin".to_string(),
23            workspace: ".".to_string(),
24        }
25    }
26}
27
28/// CLI-based platform client.
29///
30/// Calls tonin subcommands and parses JSON responses. Used by agnitiv-platform
31/// to orchestrate deployments without direct dependency on tonin internals.
32pub struct PlatformClient {
33    config: PlatformClientConfig,
34}
35
36impl PlatformClient {
37    /// Create a new platform client with default config.
38    pub fn new() -> Self {
39        Self {
40            config: PlatformClientConfig::default(),
41        }
42    }
43
44    /// Create a new platform client with custom config.
45    pub fn with_config(config: PlatformClientConfig) -> Self {
46        Self { config }
47    }
48
49    /// Deploy a service to an environment.
50    ///
51    /// Executes `tonin platform deploy --env <ENV> --service <SERVICE> --json`
52    /// and parses the JSON response.
53    ///
54    /// # Arguments
55    /// * `env` — Environment name (e.g. "staging", "prod")
56    /// * `service` — Service name to deploy
57    ///
58    /// # Returns
59    /// * `Ok(DeployResponse)` — Deployment result
60    /// * `Err(...)` — If command fails or output is not valid JSON
61    pub fn deploy_service(
62        &self,
63        env: &str,
64        service: &str,
65    ) -> Result<DeployResponse, Box<dyn std::error::Error>> {
66        let output = Command::new(&self.config.tonin_bin)
67            .args([
68                "platform",
69                "deploy",
70                "--env",
71                env,
72                "--service",
73                service,
74                "--json",
75                "--workspace",
76                &self.config.workspace,
77            ])
78            .output()?;
79
80        if !output.status.success() {
81            let stderr = String::from_utf8_lossy(&output.stderr);
82            return Err(format!("tonin platform deploy failed: {}", stderr).into());
83        }
84
85        let response: DeployResponse = serde_json::from_slice(&output.stdout)?;
86        Ok(response)
87    }
88
89    /// Get the status of all services in an environment.
90    ///
91    /// Executes `tonin platform status --env <ENV> --json` and parses the response.
92    ///
93    /// # Arguments
94    /// * `env` — Environment name
95    ///
96    /// # Returns
97    /// * `Ok(Vec<ServiceStatusResponse>)` — Status of all services
98    /// * `Err(...)` — If command fails or output is invalid
99    pub fn get_status(
100        &self,
101        env: &str,
102    ) -> Result<Vec<ServiceStatusResponse>, Box<dyn std::error::Error>> {
103        let output = Command::new(&self.config.tonin_bin)
104            .args([
105                "platform",
106                "status",
107                "--env",
108                env,
109                "--json",
110                "--workspace",
111                &self.config.workspace,
112            ])
113            .output()?;
114
115        if !output.status.success() {
116            let stderr = String::from_utf8_lossy(&output.stderr);
117            return Err(format!("tonin platform status failed: {}", stderr).into());
118        }
119
120        let response: Vec<ServiceStatusResponse> = serde_json::from_slice(&output.stdout)?;
121        Ok(response)
122    }
123
124    /// Rollback a service to its previous release.
125    ///
126    /// Executes `tonin platform rollback --service <SERVICE> --env <ENV>`.
127    ///
128    /// # Arguments
129    /// * `service` — Service name
130    /// * `env` — Environment name
131    ///
132    /// # Returns
133    /// * `Ok(())` — Rollback succeeded
134    /// * `Err(...)` — If command fails
135    pub fn rollback_service(
136        &self,
137        service: &str,
138        env: &str,
139    ) -> Result<(), Box<dyn std::error::Error>> {
140        let output = Command::new(&self.config.tonin_bin)
141            .args([
142                "platform",
143                "rollback",
144                "--service",
145                service,
146                "--env",
147                env,
148                "--workspace",
149                &self.config.workspace,
150            ])
151            .output()?;
152
153        if !output.status.success() {
154            let stderr = String::from_utf8_lossy(&output.stderr);
155            return Err(format!("tonin platform rollback failed: {}", stderr).into());
156        }
157
158        Ok(())
159    }
160}
161
162impl Default for PlatformClient {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168/// Response from a deployment command.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct DeployResponse {
171    pub service: String,
172    pub env: String,
173    pub digest: String,
174    pub status: String,
175    pub timestamp: u64,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub message: Option<String>,
178}
179
180/// Response containing service status.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ServiceStatusResponse {
183    pub service: String,
184    pub env: String,
185    pub running_version: Option<String>,
186    pub desired_version: Option<String>,
187    pub digest: Option<String>,
188    pub health: String,
189    pub timestamp: u64,
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn platform_client_config_default() {
198        let config = PlatformClientConfig::default();
199        assert_eq!(config.tonin_bin, "tonin");
200        assert_eq!(config.workspace, ".");
201    }
202
203    #[test]
204    fn platform_client_new() {
205        let client = PlatformClient::new();
206        assert_eq!(client.config.tonin_bin, "tonin");
207    }
208
209    #[test]
210    fn deploy_response_serialize() {
211        let response = DeployResponse {
212            service: "users-service".to_string(),
213            env: "staging".to_string(),
214            digest: "sha256:abc123".to_string(),
215            status: "success".to_string(),
216            timestamp: 1719792000,
217            message: None,
218        };
219
220        let json = serde_json::to_string(&response).expect("serialize failed");
221        assert!(json.contains("users-service"));
222
223        let deserialized: DeployResponse = serde_json::from_str(&json).expect("deserialize failed");
224        assert_eq!(deserialized.status, "success");
225    }
226
227    #[test]
228    fn service_status_response_serialize() {
229        let status = ServiceStatusResponse {
230            service: "orders-service".to_string(),
231            env: "prod".to_string(),
232            running_version: Some("1.2.3".to_string()),
233            desired_version: Some("1.2.3".to_string()),
234            digest: Some("sha256:def456".to_string()),
235            health: "healthy".to_string(),
236            timestamp: 1719792000,
237        };
238
239        let json = serde_json::to_string(&status).expect("serialize failed");
240        let deserialized: ServiceStatusResponse =
241            serde_json::from_str(&json).expect("deserialize failed");
242        assert_eq!(deserialized.health, "healthy");
243    }
244}