Skip to main content

stormchaser_cli/commands/
storage.rs

1use crate::utils::{handle_response, require_token};
2use anyhow::Result;
3use clap::Subcommand;
4use serde_json::json;
5use serde_json::Value;
6use std::fs;
7use std::path::PathBuf;
8
9#[derive(Subcommand)]
10pub enum StorageCommands {
11    /// List storage backends
12    List,
13    /// Create a storage backend
14    Create {
15        name: String,
16        /// The type of storage backend (e.g., s3, oci)
17        #[arg(long)]
18        backend_type: String,
19        /// Path to JSON configuration file
20        #[arg(long)]
21        config: PathBuf,
22        #[arg(long)]
23        default_sfs: bool,
24        #[arg(long)]
25        description: Option<String>,
26        /// Optional AWS role ARN to assume
27        #[arg(long)]
28        aws_assume_role_arn: Option<String>,
29    },
30    /// Get storage backend details
31    Get { id: stormchaser_model::BackendId },
32    /// Update a storage backend
33    Update {
34        id: stormchaser_model::BackendId,
35        #[arg(long)]
36        name: Option<String>,
37        /// Path to JSON configuration file
38        #[arg(long)]
39        config: Option<PathBuf>,
40        #[arg(long)]
41        default_sfs: Option<bool>,
42        #[arg(long)]
43        description: Option<String>,
44        /// Optional AWS role ARN to assume
45        #[arg(long)]
46        aws_assume_role_arn: Option<String>,
47    },
48    /// Delete a storage backend
49    Delete { id: stormchaser_model::BackendId },
50}
51
52pub async fn handle(
53    url: &str,
54    token: Option<&str>,
55    http_client: &reqwest_middleware::ClientWithMiddleware,
56    command: StorageCommands,
57) -> Result<()> {
58    match command {
59        StorageCommands::List => {
60            let token = require_token(token)?;
61            let res = http_client
62                .get(format!("{}/api/v1/storage-backends", url))
63                .header("Authorization", format!("Bearer {}", token))
64                .send()
65                .await?;
66            handle_response(res).await?;
67        }
68        StorageCommands::Create {
69            name,
70            backend_type,
71            config,
72            default_sfs,
73            description,
74            aws_assume_role_arn,
75        } => {
76            let config_json: Value = serde_json::from_str(&fs::read_to_string(config)?)?;
77            let token = require_token(token)?;
78            let res = http_client
79                .post(format!("{}/api/v1/storage-backends", url))
80                .header("Authorization", format!("Bearer {}", token))
81                .json(&json!({
82                    "name": name,
83                    "backend_type": backend_type,
84                    "config": config_json,
85                    "is_default_sfs": default_sfs,
86                    "description": description,
87                    "aws_assume_role_arn": aws_assume_role_arn,
88                }))
89                .send()
90                .await?;
91            handle_response(res).await?;
92        }
93        StorageCommands::Get { id } => {
94            let token = require_token(token)?;
95            let res = http_client
96                .get(format!("{}/api/v1/storage-backends/{}", url, id))
97                .header("Authorization", format!("Bearer {}", token))
98                .send()
99                .await?;
100            handle_response(res).await?;
101        }
102        StorageCommands::Update {
103            id,
104            name,
105            config,
106            default_sfs,
107            description,
108            aws_assume_role_arn,
109        } => {
110            let mut body = json!({});
111            if let Some(n) = name {
112                body["name"] = json!(n);
113            }
114            if let Some(c) = config {
115                body["config"] = serde_json::from_str(&fs::read_to_string(c)?)?;
116            }
117            if let Some(d) = default_sfs {
118                body["is_default_sfs"] = json!(d);
119            }
120            if let Some(desc) = description {
121                body["description"] = json!(desc);
122            }
123            if let Some(arn) = aws_assume_role_arn {
124                body["aws_assume_role_arn"] = json!(arn);
125            }
126
127            let token = require_token(token)?;
128            let res = http_client
129                .patch(format!("{}/api/v1/storage-backends/{}", url, id))
130                .header("Authorization", format!("Bearer {}", token))
131                .json(&body)
132                .send()
133                .await?;
134            handle_response(res).await?;
135        }
136        StorageCommands::Delete { id } => {
137            let token = require_token(token)?;
138            let res = http_client
139                .delete(format!("{}/api/v1/storage-backends/{}", url, id))
140                .header("Authorization", format!("Bearer {}", token))
141                .send()
142                .await?;
143            handle_response(res).await?;
144        }
145    }
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use reqwest_middleware::ClientBuilder;
153    use wiremock::matchers::{header, method, path};
154    use wiremock::{Mock, MockServer, ResponseTemplate};
155
156    #[tokio::test]
157    async fn test_storage_list() {
158        let server = MockServer::start().await;
159        Mock::given(method("GET"))
160            .and(path("/api/v1/storage-backends"))
161            .and(header("Authorization", "Bearer test-token"))
162            .respond_with(ResponseTemplate::new(200).set_body_json(json!([])))
163            .mount(&server)
164            .await;
165
166        let client = ClientBuilder::new(reqwest::Client::new()).build();
167        let cmd = StorageCommands::List;
168
169        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
170        assert!(result.is_ok());
171    }
172
173    #[tokio::test]
174    async fn test_storage_create() {
175        let server = MockServer::start().await;
176        Mock::given(method("POST"))
177            .and(path("/api/v1/storage-backends"))
178            .and(header("Authorization", "Bearer test-token"))
179            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "created"})))
180            .mount(&server)
181            .await;
182
183        use std::io::Write;
184        use tempfile::NamedTempFile;
185        let mut temp_file = NamedTempFile::new().unwrap();
186        writeln!(temp_file, "{{\"bucket\":\"my-bucket\"}}").unwrap();
187
188        let client = ClientBuilder::new(reqwest::Client::new()).build();
189        let cmd = StorageCommands::Create {
190            name: "test-storage".to_string(),
191            backend_type: "s3".to_string(),
192            config: temp_file.path().to_path_buf(),
193            default_sfs: true,
194            description: None,
195            aws_assume_role_arn: None,
196        };
197
198        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
199        assert!(result.is_ok());
200    }
201
202    #[tokio::test]
203    async fn test_storage_delete() {
204        let server = MockServer::start().await;
205        let id = stormchaser_model::BackendId::new_v4();
206        Mock::given(method("DELETE"))
207            .and(path(format!("/api/v1/storage-backends/{}", id)))
208            .and(header("Authorization", "Bearer test-token"))
209            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "deleted"})))
210            .mount(&server)
211            .await;
212
213        let client = ClientBuilder::new(reqwest::Client::new()).build();
214        let cmd = StorageCommands::Delete { id };
215
216        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
217        assert!(result.is_ok());
218    }
219}